From 5ad3304db30568946bbc1da8d7c0dcb0ac832a36 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 16:03:20 +0000 Subject: [PATCH 01/29] refactor(commons): move LocalCache and its group out of the Android app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `LocalCache` (4,021 lines) had no Android imports and no app-package types — only six references to the shell around it. It moves to `commons/jvmAndroid/…/commons/model/cache/`, with `AntiSpamFilter`, `CachePruner`, `CacheSearch`, `DvmHeartbeatRegistry` and `OnchainZapResolver`. This is part A of the sweep plan's Wave 2; `desktopApp` still runs its own `DesktopLocalCache` fork and is untouched here. The six references become `LocalCacheHost`, a port collecting what the cache cannot supply for itself: a scope, `isDebug`, the NIP-95 blob directory, the relay stats sink, the relay's own pubkey and the main-thread assertion. Every member defaults to the honest answer for a host that supplies none of them, so the cache runs shell-less; Android installs `AmethystLocalCacheHost` from `AppModules` next to `val cache`, before any event is consumed. The whole seam cut is 101 changed lines. `dateFormatter` repoints to the twin that already existed in `commons/util`. `nip95BlobDir` is nullable, so `consume(FileStorageEvent)` grows one branch: with nowhere to spill the blob, the note keeps the event whole rather than loading a content-stripped copy whose bytes are on no disk. Android always supplies the directory, so its behaviour is unchanged. The plan flagged `CachePruner`/`CacheSearch` taking `Account` as needing a design decision. `IAccount` needed no new members and Desktop needed no change: `isHidden`, `hiddenWordsCase` and `followingKeySet()` are already on it, and `isFollowing(user)` is `user.pubkeyHex in followingKeySet()`. `pruneHiddenEvents` takes `LiveHiddenUsers` by value instead — `IAccount` exposes hidden users only as hash codes, which would prune authors the reader never muted. Also: `ILocalCache` was declared inline in `LocalCache.kt` and used nowhere else, so it is hoisted to commonMain as the write-side port beside the read-side `ICacheProvider`; `AntiSpamFilter` swaps `android.util.LruCache` for `androidx.collection.LruCache`, whose Kotlin-nullable `get` makes the two duplicate checks read each entry once into a local; and `njumpLink` moves to `commons/util/ExternalLinks.kt` so the filter can log one. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../amethyst/ImageUploadTesting.kt | 2 +- .../NotificationFeedFilterModeOverrideTest.kt | 2 +- .../ThreadDualAxisChartAssemblerTest.kt | 2 +- .../amethyst/AmethystLocalCacheHost.kt | 55 ++++++++++ .../com/vitorpamplona/amethyst/AppModules.kt | 10 +- .../com/vitorpamplona/amethyst/DebugUtils.kt | 2 +- .../consent/SignerConsentActivity.kt | 2 +- .../amethyst/favorites/FavoriteAppLauncher.kt | 2 +- .../favorites/FavoriteNostrAppPreloader.kt | 2 +- .../amethyst/favorites/NappletFavoriteIcon.kt | 2 +- .../vitorpamplona/amethyst/model/Account.kt | 1 + .../model/AccountRelayGroupActions.kt | 1 + .../amethyst/model/AccountZapActions.kt | 1 + .../amethyst/model/DvmHeartbeat.kt | 2 + .../model/GitPullRequestUpdateIndex.kt | 3 +- .../amethyst/model/GitStatusIndex.kt | 3 +- .../amethyst/model/ParticipantListBuilder.kt | 1 + .../model/accountsCache/AccountCacheState.kt | 2 +- .../bolt12Offers/Bolt12OfferListState.kt | 2 +- .../model/buzz/BuzzMembershipNotices.kt | 2 +- .../model/buzz/ChannelInvitesState.kt | 2 +- .../edits/PrivateStorageRelayListState.kt | 2 +- .../model/localRelays/LocalRelayListState.kt | 2 +- .../model/marmot/KeyPackageRelayListState.kt | 2 +- .../nip01UserMetadata/UserMetadataState.kt | 2 +- .../DeclaredFollowsPerOutboxRelay.kt | 2 +- .../DeclaredFollowsPerUsingRelay.kt | 2 +- .../FollowListOutboxOrProxyRelays.kt | 2 +- .../FollowListReusedOutboxOrProxyRelays.kt | 2 +- .../nip02FollowLists/FollowsPerOutboxRelay.kt | 2 +- .../nip02FollowLists/Kind3FollowListState.kt | 2 +- .../IncomingOtsEventVerifier.kt | 2 +- .../amethyst/model/nip03Timestamp/OtsState.kt | 2 +- .../model/nip17Dms/DmRelayListState.kt | 2 +- .../nip30CustomEmojis/OwnedEmojiPacksState.kt | 2 +- .../model/nip46Signer/Nip46ConsentBridge.kt | 2 +- .../nip47WalletConnect/NwcSignerState.kt | 2 +- .../amethyst/model/nip51Lists/PinListState.kt | 2 +- .../blockPeopleList/BlockPeopleListState.kt | 2 +- .../blockedRelays/BlockedRelayListState.kt | 2 +- .../BroadcastRelayListState.kt | 2 +- .../FavoriteAlgoFeedsListState.kt | 2 +- .../geohashLists/GeohashListState.kt | 2 +- .../hashtagLists/HashtagListState.kt | 2 +- .../indexerRelays/IndexerRelayListState.kt | 2 +- .../interestSets/InterestSetsState.kt | 2 +- .../LabeledBookmarkListsState.kt | 2 +- .../nip51Lists/muteList/MuteListState.kt | 2 +- .../nip51Lists/peopleList/FollowListsState.kt | 2 +- .../nip51Lists/peopleList/PeopleListsState.kt | 2 +- .../proxyRelays/ProxyRelayListState.kt | 2 +- .../relayFeeds/RelayFeedListState.kt | 2 +- .../searchRelays/SearchRelayListState.kt | 2 +- .../trustedRelays/TrustedRelayListState.kt | 2 +- .../nip60Cashu/CashuMintDirectoryState.kt | 2 +- .../model/nip60Cashu/CashuWalletState.kt | 2 +- .../model/nip62Vanish/VanishRequestsState.kt | 2 +- .../nip65RelayList/Nip65RelayListState.kt | 2 +- .../nip72Communities/CommunityListState.kt | 2 +- .../nip78AppSpecific/AppSpecificState.kt | 2 +- .../AppRecommendationsState.kt | 2 +- .../NipA3PaymentTargetsState.kt | 2 +- .../topNavFeeds/FeedTopNavFilterState.kt | 2 +- .../TrustProviderListState.kt | 2 +- .../amethyst/model/zap/RailCapability.kt | 2 +- .../amethyst/napplet/NappletLauncher.kt | 2 +- .../amethyst/napplet/NostrSignerOpLabels.kt | 2 +- .../amethyst/service/ZapPaymentHandler.kt | 2 +- .../calendar/CalendarReminderWorker.kt | 2 +- .../service/call/notification/CallNotifier.kt | 2 +- .../crashreports/DisplayCrashMessages.kt | 2 +- .../eventCache/MemoryTrimmingService.kt | 4 +- .../AlwaysOnNotificationServiceManager.kt | 2 +- .../EventNotificationConsumer.kt | 2 +- .../notifications/NotificationContent.kt | 2 +- .../notifications/NotificationDispatcher.kt | 2 +- .../notifications/NotificationEnricher.kt | 2 +- .../NotificationReplyReceiver.kt | 2 +- .../service/notifications/PokeyReceiver.kt | 2 +- .../notifications/PushWrapDecryptor.kt | 2 +- .../renderers/ArticleNotification.kt | 2 +- .../renderers/BadgeNotification.kt | 2 +- .../renderers/BuzzDmNotification.kt | 2 +- .../renderers/ChessNotification.kt | 2 +- .../renderers/CodeNotification.kt | 2 +- .../renderers/DirectMessageNotification.kt | 2 +- .../renderers/GroupMessageNotification.kt | 2 +- .../renderers/MediaNotification.kt | 2 +- .../renderers/MentionNotification.kt | 2 +- .../renderers/ReactionNotification.kt | 2 +- .../renderers/ReplyNotification.kt | 2 +- .../renderers/RepostNotification.kt | 2 +- .../renderers/ZapNotification.kt | 2 +- .../relayClient/CacheClientConnector.kt | 2 +- .../compose/RelayAuthPromptHost.kt | 2 +- .../RelaySubscriptionsCoordinator.kt | 2 +- .../AccountForegroundFilterAssembler.kt | 2 +- .../AccountFollowsLoaderSubAssembler.kt | 2 +- .../follows/FilterFindFollowMetadataForKey.kt | 2 +- .../DisplayResourceUsageAlert.kt | 2 +- .../workouts/health/PublishedWorkouts.kt | 2 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 2 +- .../actions/UrlUserTagOutputTransformation.kt | 2 +- .../amethyst/ui/components/ClickableRoute.kt | 2 +- .../amethyst/ui/components/RichTextViewer.kt | 2 +- .../markdown/RenderContentAsMarkdown.kt | 2 +- .../namecoin/NamecoinResolutionRow.kt | 2 +- .../components/toasts/multiline/ErrorList.kt | 2 +- .../multiline/MultiUserErrorMessageDialog.kt | 2 +- .../bottombars/GroupBottomBarEntries.kt | 2 +- .../drawer/AccountSwitchBottomSheet.kt | 2 +- .../ui/navigation/routes/RouteMaker.kt | 2 +- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../amethyst/ui/note/NoteQuickActionMenu.kt | 5 +- .../amethyst/ui/note/OnchainZapGallery.kt | 2 +- .../amethyst/ui/note/ZapPollNote.kt | 2 +- .../userSuggestions/ShowUserSuggestionList.kt | 2 +- .../ui/note/elements/DisplayCommunity.kt | 2 +- .../note/elements/NoteHeaderMarkersPreview.kt | 2 +- .../ui/note/elements/StaleRelayHint.kt | 2 +- .../nip22Comments/CommentPostViewModel.kt | 2 +- .../nip22Comments/DisplayCommunityScope.kt | 2 +- .../ui/note/types/ActivityCardPreviews.kt | 2 +- .../amethyst/ui/note/types/Attestation.kt | 2 +- .../amethyst/ui/note/types/Badge.kt | 2 +- .../amethyst/ui/note/types/CalendarEvent.kt | 2 +- .../amethyst/ui/note/types/CalendarRsvpRow.kt | 2 +- .../amethyst/ui/note/types/ChannelInvite.kt | 2 +- .../amethyst/ui/note/types/ContactList.kt | 2 +- .../amethyst/ui/note/types/FollowList.kt | 2 +- .../ui/note/types/GitStatusActions.kt | 2 +- .../amethyst/ui/note/types/LiveActivity.kt | 2 +- .../amethyst/ui/note/types/MeetingSpace.kt | 2 +- .../amethyst/ui/note/types/MusicPlaylist.kt | 2 +- .../amethyst/ui/note/types/MusicTrack.kt | 2 +- .../amethyst/ui/note/types/Nip.kt | 2 +- .../amethyst/ui/note/types/OnchainZapEvent.kt | 2 +- .../amethyst/ui/note/types/Poll.kt | 2 +- .../ui/note/types/ProfileCardPreview.kt | 2 +- .../ui/note/types/RelayGroupMessage.kt | 2 +- .../amethyst/ui/note/types/SoftwareApp.kt | 2 +- .../amethyst/ui/note/types/Text.kt | 2 +- .../amethyst/ui/note/types/Torrent.kt | 2 +- .../amethyst/ui/note/types/TorrentComment.kt | 2 +- .../amethyst/ui/note/types/ZapEvent.kt | 2 +- .../amethyst/ui/note/types/ZapPoll.kt | 2 +- .../ui/screen/AndroidFeedViewModel.kt | 2 +- .../amethyst/ui/screen/UserFeedViewModel.kt | 2 +- .../loggedIn/AccountFeedContentStates.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 4 +- .../loggedIn/DecryptAndIndexProcessor.kt | 2 +- .../ProfileAppRecommendationsScreen.kt | 2 +- .../articles/dal/ArticlesFeedFilter.kt | 2 +- .../badges/award/AwardBadgeViewModel.kt | 2 +- .../loggedIn/badges/dal/BadgesFeedFilter.kt | 2 +- .../badges/profile/ProfileBadgesScreen.kt | 2 +- .../loggedIn/buzz/AgentAttestationScreen.kt | 2 +- .../loggedIn/buzz/AgentConsoleViewModel.kt | 2 +- .../buzz/AgentPersonaEditViewModel.kt | 2 +- .../loggedIn/buzz/BuzzAddPeopleDialog.kt | 2 +- .../screen/loggedIn/buzz/BuzzCanvasScreen.kt | 2 +- .../screen/loggedIn/buzz/BuzzDmDiscovery.kt | 2 +- .../screen/loggedIn/buzz/BuzzDmListScreen.kt | 2 +- .../loggedIn/buzz/BuzzDmListViewModel.kt | 2 +- .../loggedIn/buzz/BuzzForumThreadScreen.kt | 2 +- .../ui/screen/loggedIn/buzz/BuzzImportRow.kt | 2 +- .../screen/loggedIn/buzz/BuzzNewDmScreen.kt | 2 +- .../loggedIn/buzz/BuzzNewDmViewModel.kt | 2 +- .../loggedIn/buzz/BuzzRelayImportViewModel.kt | 2 +- .../calendars/CalendarCollectionsView.kt | 2 +- .../loggedIn/calendars/CalendarFilterChip.kt | 2 +- .../create/NewCalendarCollectionViewModel.kt | 2 +- .../create/NewCalendarEventViewModel.kt | 2 +- .../dal/CalendarAppointmentsFeedFilter.kt | 2 +- .../dal/CalendarCollectionsFeedFilter.kt | 2 +- .../calendars/detail/AddToCalendarSheet.kt | 2 +- .../detail/CalendarEventDetailScreen.kt | 2 +- .../marmotGroup/MarmotGroupFeedViewModel.kt | 2 +- .../loggedIn/chats/minichat/MinichatScreen.kt | 2 +- .../privateDM/dal/ChatroomFeedViewModel.kt | 2 +- .../privateDM/datasource/FilterNip04DMs.kt | 2 +- .../header/NewChatroomSubjectDialog.kt | 2 +- .../privateDM/send/ChatNewMessageViewModel.kt | 2 +- .../send/PrivateMessageEditFieldRow.kt | 2 +- .../privateDM/send/upload/ChatFileSender.kt | 2 +- .../concord/ConcordChannelListScreen.kt | 2 +- .../concord/ConcordChannelScreen.kt | 2 +- .../concord/ConcordHomeScreen.kt | 2 +- .../publicChannels/concord/ConcordUnread.kt | 2 +- .../ConcordChannelHistoryFilterAssembler.kt | 2 +- .../send/ConcordNewMessageViewModel.kt | 2 +- .../dal/ChannelFeedViewModel.kt | 2 +- .../header/actions/ShareChatButton.kt | 2 +- .../metadata/ChannelMetadataViewModel.kt | 2 +- .../header/LongLiveActivityChannelHeader.kt | 2 +- .../header/ShortLiveActivityChannelHeader.kt | 2 +- .../relayGroup/RelayGroupChannelListScreen.kt | 2 +- .../relayGroup/RelayGroupParentPicker.kt | 2 +- .../relayGroup/RelayGroupTopBar.kt | 4 +- .../relayGroup/RelayGroupUnread.kt | 2 +- .../dal/RelayGroupDiscoveryFeedFilter.kt | 2 +- ...elayGroupOpenChatHistoryFilterAssembler.kt | 2 +- ...yGroupOpenThreadsHistoryFilterAssembler.kt | 2 +- .../RelayGroupsDiscoverySubAssembler.kt | 2 +- .../send/ChannelNewMessageViewModel.kt | 2 +- .../chats/rooms/ChatroomHeaderCompose.kt | 2 +- .../rooms/dal/ChatroomListKnownFeedFilter.kt | 2 +- .../rooms/dal/ChatroomListNewFeedFilter.kt | 2 +- .../datasource/FilterFollowingPublicChats.kt | 2 +- .../FilterLastMessageFollowingPublicChats.kt | 2 +- .../FollowingGeohashChatSubAssembler.kt | 2 +- .../loggedIn/chats/share/ShareToDMScreen.kt | 2 +- .../loggedIn/chess/AndroidChessAdapter.kt | 2 +- .../loggedIn/chess/dal/ChessFeedFilter.kt | 2 +- .../communities/dal/CommunityFeedFilter.kt | 2 +- .../dal/CommunityModerationFeedFilter.kt | 2 +- .../communities/dal/CommunityRulesLookup.kt | 2 +- .../list/dal/CommunitiesFeedFilter.kt | 2 +- .../newCommunity/NewCommunityScreen.kt | 2 +- .../DiscoverLongFormFeedFilter.kt | 2 +- .../nip23LongForm/LongFormPostViewModel.kt | 2 +- .../nip28Chats/DiscoverChatFeedFilter.kt | 2 +- .../DiscoverFollowSetsFeedFilter.kt | 2 +- .../discover/nip51FollowSets/FollowSetCard.kt | 2 +- .../DiscoverLiveFeedFilter.kt | 2 +- .../nip53LiveActivities/LiveActivityCard.kt | 2 +- .../nip72Communities/CommunityCard.kt | 2 +- .../DiscoverCommunityFeedFilter.kt | 2 +- .../nip90DVMs/DiscoverNIP89FeedFilter.kt | 2 +- .../DiscoverMarketplaceFeedFilter.kt | 2 +- .../nip99Classifieds/NewProductViewModel.kt | 2 +- .../drafts/dal/DraftEventsFeedFilter.kt | 2 +- .../loggedIn/dvms/DvmHeartbeatObservation.kt | 2 +- .../ui/screen/loggedIn/dvms/DvmTopBar.kt | 2 +- .../NIP90ContentDiscoveryResponseFilter.kt | 2 +- .../embed/FavoriteAppManifestPreloader.kt | 2 +- .../browse/dal/BrowseEmojiSetsFeedFilter.kt | 2 +- .../FollowPackFeedConversationsFeedFilter.kt | 2 +- .../dal/FollowPackFeedNewThreadFeedFilter.kt | 2 +- .../feed/dal/FollowPackMembersFeedFilter.kt | 2 +- .../loggedIn/geohash/dal/GeoHashFeedFilter.kt | 2 +- .../geohash/dal/GeoHashFeedViewModel.kt | 2 +- .../loggedIn/gitRepo/GitRepositoryHomeUi.kt | 2 +- .../gitRepo/dal/RepositoryIssuesFeedFilter.kt | 2 +- .../dal/RepositoryPatchesFeedFilter.kt | 2 +- .../dal/GitRepositoriesFeedFilter.kt | 2 +- .../loggedIn/hashtag/dal/HashtagFeedFilter.kt | 2 +- .../hashtag/dal/HashtagFeedViewModel.kt | 2 +- .../hashtag/datasource/FilterHashtagLabels.kt | 2 +- .../highlights/dal/HighlightsFeedFilter.kt | 2 +- .../loggedIn/home/ShortNotePostViewModel.kt | 2 +- .../home/dal/HomeConversationsFeedFilter.kt | 2 +- .../loggedIn/home/dal/HomeLiveFilter.kt | 2 +- .../home/dal/HomeNewThreadFeedFilter.kt | 2 +- .../loggedIn/home/live/LiveStatusIndicator.kt | 2 +- .../home/live/RenderLiveActivityBubble.kt | 2 +- .../lists/display/ShowUserSuggestions.kt | 2 +- .../lists/display/lists/PeopleListScreen.kt | 2 +- .../lists/display/packs/FollowPackScreen.kt | 2 +- .../loggedIn/lists/list/PeopleListItem.kt | 2 +- .../FollowListAndPackAndUserScreen.kt | 2 +- .../livestreams/dal/LiveStreamsFeedFilter.kt | 2 +- .../loggedIn/longs/dal/LongsFeedFilter.kt | 2 +- .../music/AddToMusicPlaylistViewModel.kt | 2 +- .../music/NewMusicPlaylistViewModel.kt | 2 +- .../loggedIn/music/NewMusicTrackViewModel.kt | 2 +- .../music/dal/MusicPlaylistsFeedFilter.kt | 2 +- .../music/dal/MusicTracksFeedFilter.kt | 2 +- .../loggedIn/napplets/ConnectedAppsScreen.kt | 2 +- .../screen/loggedIn/nests/NestsFeedLoaded.kt | 2 +- .../loggedIn/nests/dal/NestsFeedFilter.kt | 2 +- .../room/chat/NestNewMessageViewModel.kt | 2 +- .../room/lifecycle/NestRoomEventCollectors.kt | 2 +- .../loggedIn/nests/room/lobby/NestJoinCard.kt | 2 +- .../nests/room/lobby/NestLobbyScreen.kt | 2 +- .../ParticipantHostActionsSheet.kt | 2 +- .../nests/room/stage/HandRaiseQueueSection.kt | 2 +- .../nests/room/stage/ParticipantsGrid.kt | 2 +- .../ImportFollowListSelectUserScreen.kt | 2 +- .../notifications/CardFeedContentState.kt | 4 +- .../notifications/NotificationSummaryState.kt | 2 +- .../dal/NotificationFeedFilter.kt | 2 +- .../notifications/donations/ZapTheDevsCard.kt | 2 +- .../NewPublicMessageViewModel.kt | 2 +- .../pictures/PictureCardComposePreview.kt | 2 +- .../pictures/dal/PictureFeedFilter.kt | 2 +- .../loggedIn/podcasts/PodcastAuthors.kt | 2 +- .../screen/loggedIn/podcasts/PodcastScreen.kt | 2 +- .../authoring/EditPodcastShowViewModel.kt | 2 +- .../authoring/NewPodcastEpisodeViewModel.kt | 2 +- .../authoring/PodcastAuthoringScreen.kt | 2 +- .../dal/OnePodcastEpisodesFeedFilter.kt | 2 +- .../podcasts/dal/OnePodcastFeedViewModel.kt | 2 +- .../podcasts/dal/PodcastEpisodesFeedFilter.kt | 2 +- .../podcasts/dal/PodcastsFeedFilter.kt | 2 +- .../polls/dal/ClosedPollsFeedFilter.kt | 2 +- .../loggedIn/polls/dal/OpenPollsFeedFilter.kt | 2 +- .../loggedIn/polls/dal/PollsFeedFilter.kt | 2 +- .../products/dal/ProductsFeedFilter.kt | 2 +- .../screen/loggedIn/profile/ProfileScreen.kt | 2 +- .../apps/dal/UserProfileAppsFeedFilter.kt | 2 +- .../dal/UserProfileBookmarksFeedFilter.kt | 2 +- .../dal/UserProfileConversationsFeedFilter.kt | 2 +- .../UserProfileFollowsUserFeedViewModel.kt | 2 +- .../dal/UserProfileGalleryFeedFilter.kt | 2 +- .../profile/header/DisplayPaymentTargets.kt | 2 +- .../profile/header/ProfilePaymentRailChips.kt | 2 +- ...UserProfileAppRecommendationsFeedFilter.kt | 2 +- .../profile/header/badges/DisplayBadges.kt | 2 +- .../UserExternalIdentitiesViewModel.kt | 2 +- .../mutual/dal/UserProfileMutualFeedFilter.kt | 2 +- .../dal/UserProfileNewThreadFeedFilter.kt | 2 +- .../profile/payment/SendPaymentScreen.kt | 2 +- .../dal/UserProfilePinnedNotesFeedFilter.kt | 2 +- .../loggedIn/profile/zaps/ZapNoteCompose.kt | 2 +- .../zaps/dal/UserProfileZapsViewModel.kt | 2 +- .../publicChats/dal/PublicChatsFeedFilter.kt | 2 +- .../loggedIn/relay/dal/RelayFeedFilter.kt | 2 +- .../relay/dal/SingleRelayFeedViewModel.kt | 2 +- .../loggedIn/relays/RelayInformationScreen.kt | 2 +- .../relays/common/RelaySuggestionState.kt | 2 +- .../connected/ConnectedRelayListViewModel.kt | 2 +- .../relays/nip86/RelayManagementScreen.kt | 2 +- .../relays/nip86/RelayManagementViewModel.kt | 2 +- .../ActiveSubscriptionsScreen.kt | 2 +- .../loggedIn/search/SearchBarViewModel.kt | 2 +- .../ui/screen/loggedIn/search/SearchScreen.kt | 2 +- .../loggedIn/settings/ResourceUsageScreen.kt | 2 +- .../loggedIn/settings/StringFeedViewModel.kt | 2 +- .../settings/dal/HiddenAccountsFeedFilter.kt | 2 +- .../settings/dal/MutedThreadsFeedFilter.kt | 2 +- .../settings/dal/SpammerAccountsFeedFilter.kt | 2 +- .../loggedIn/shorts/dal/ShortsFeedFilter.kt | 2 +- .../dal/SoftwareAppsFeedFilter.kt | 2 +- .../loggedIn/threadview/ThreadFeedView.kt | 2 +- .../dal/AndroidLevelFeedViewModel.kt | 2 +- .../threadview/dal/ThreadFeedViewModel.kt | 2 +- .../screen/loggedIn/url/dal/UrlFeedFilter.kt | 2 +- .../loggedIn/url/dal/UrlFeedViewModel.kt | 2 +- .../loggedIn/video/dal/VideoFeedFilter.kt | 2 +- .../wallet/CashuMintRecommendationsScreen.kt | 2 +- .../loggedIn/wallet/CashuMintsScreen.kt | 2 +- .../loggedIn/wallet/CashuWalletScreen.kt | 2 +- .../wallet/OnchainTransactionsViewModel.kt | 2 +- .../loggedIn/wallet/OnchainZapSendDialog.kt | 2 +- .../wallet/wizard/CashuWalletDiscovery.kt | 2 +- .../webBookmarks/dal/WebBookmarkFeedFilter.kt | 2 +- .../workouts/dal/WorkoutFeedFilter.kt | 2 +- .../amethyst/NewMessageTaggerKeyParseTest.kt | 2 +- .../CalendarReminderCandidatesTest.kt | 2 +- .../model/AppSpecificDataRepublishTest.kt | 1 + .../model/BuzzDmNotificationResolutionTest.kt | 1 + .../model/BuzzWorkspaceChannelTest.kt | 1 + .../amethyst/model/DvmHeartbeatTest.kt | 2 + .../model/EntityRatingIngestionTest.kt | 1 + .../model/LocalCacheSearchParityTest.kt | 1 + .../model/ReportNamingIndexIngestionTest.kt | 1 + .../BlockedRelayListAddRelayTest.kt | 2 +- .../FavoriteAlgoFeedTopNavFilterTest.kt | 4 +- .../NotificationContentMentionTest.kt | 2 +- ...ssableAuthorRelayLoaderSubAssemblerTest.kt | 2 +- .../2026-08-30-commons-migration-sweep.md | 102 +++++++++++++++++- .../commons/model/cache/ILocalCache.kt | 43 ++++++++ .../amethyst/commons/util/ExternalLinks.kt | 28 +++++ .../commons/model/cache}/AntiSpamFilter.kt | 44 ++++---- .../commons/model/cache}/CachePruner.kt | 29 +++-- .../commons/model/cache}/CacheSearch.kt | 18 ++-- .../model/cache/LargeSoftCacheAddressExt.kt | 1 - .../commons/model/cache}/LocalCache.kt | 101 ++++++++--------- .../commons/model/cache/LocalCacheHost.kt | 81 ++++++++++++++ .../model/nip90DVMs}/DvmHeartbeatRegistry.kt | 2 +- .../nipBCOnchainZaps/OnchainZapResolver.kt | 20 ++-- 372 files changed, 794 insertions(+), 463 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ILocalCache.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/ExternalLinks.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/model => commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache}/AntiSpamFilter.kt (84%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/model => commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache}/CachePruner.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/model => commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache}/CacheSearch.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/model => commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache}/LocalCache.kt (98%) create mode 100644 commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/model => commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip90DVMs}/DvmHeartbeatRegistry.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons}/model/nipBCOnchainZaps/OnchainZapResolver.kt (94%) diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt index 8e8e4fcd67..2263cccba6 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt @@ -25,9 +25,9 @@ import android.graphics.Color import androidx.core.graphics.createBitmap import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.http.DefaultContentTypeInterceptor import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState import com.vitorpamplona.amethyst.service.uploads.FileHeader import com.vitorpamplona.amethyst.service.uploads.ImageDownloader diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/NotificationFeedFilterModeOverrideTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/NotificationFeedFilterModeOverrideTest.kt index 9c72eead2d..d51726d393 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/NotificationFeedFilterModeOverrideTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/NotificationFeedFilterModeOverrideTest.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.commons.relayClient.assemblers.CashuMintDirectoryFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler 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.notifications.dal.NotificationFeedFilter diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt index 39b9db024d..a1dae99bcc 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt @@ -22,12 +22,12 @@ package com.vitorpamplona.amethyst import androidx.test.ext.junit.runners.AndroidJUnit4 import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.assemblers.CashuMintDirectoryFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.commons.viewmodels.thread.ThreadFeedFilter 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.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt new file mode 100644 index 0000000000..0d7fd603b1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.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 + +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCacheHost +import com.vitorpamplona.amethyst.service.checkNotInMainThread +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 kotlinx.coroutines.CoroutineScope +import java.io.File + +/** + * Binds the shared [LocalCache] in `commons` to this app's shell. + * + * Every member reads through to [modules] on each call rather than capturing a value, because + * three of the four are lazy and are built long after the cache singleton exists. + */ +class AmethystLocalCacheHost( + private val modules: AppModules, + /** + * Passed in rather than read here: the top-level `isDebug` and this override share a name, + * so inside the class body the name would resolve to the override itself. + */ + override val isDebug: Boolean, +) : LocalCacheHost { + override val scope: CoroutineScope get() = modules.applicationIOScope + + override val nip95BlobDir: File get() = modules.nip95cache + + override val relayStats: RelayStats get() = modules.relayStats + + override fun relaySelfPubKey(relay: NormalizedRelayUrl): HexKey? = modules.nip11Cache.getFromCache(relay).self + + override fun assertNotMainThread() = checkNotInMainThread() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index 7fd2db22f8..2f69becc8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -28,6 +28,7 @@ import androidx.security.crypto.EncryptedSharedPreferences import coil3.disk.DiskCache import coil3.memory.MemoryCache import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip03Timestamp.BitcoinExplorerEndpoint import com.vitorpamplona.amethyst.commons.model.nip03Timestamp.TorAwareOkHttpOtsResolverBuilder import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger @@ -56,7 +57,6 @@ import com.vitorpamplona.amethyst.commons.tor.TorSettings import com.vitorpamplona.amethyst.connectedApps.DataStoreNostrSignerPermissionStore import com.vitorpamplona.amethyst.connectedApps.nip46.DataStoreNip46ClientStore import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.UiSettings import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.model.nip03Timestamp.IncomingOtsEventVerifier @@ -702,6 +702,14 @@ class AppModules( // Caches all events in Memory val cache: LocalCache = LocalCache + // The cache lives in `commons` and knows nothing about this app. Hand it the pieces it + // cannot supply for itself — a scope, the NIP-95 blob directory, the relay identity and + // stats sinks, the debug flag and the main-thread assertion — before anything consumes an + // event, which is why this sits next to the cache rather than in a later init block. + init { + cache.appHost = AmethystLocalCacheHost(this, isDebug) + } + // NIP-BC onchain zap verification backend. Wired up once at app init so // LocalCache.consume(OnchainZapEvent) can sum the on-chain output values // that pay the recipient's derived Taproot address. Wrapped in a caching diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt index 9735216a02..0a1b68b98f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt @@ -25,7 +25,7 @@ import android.content.Context import android.content.pm.ApplicationInfo import android.os.Debug import androidx.core.content.getSystemService -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizedUrls import com.vitorpamplona.quartz.utils.Log diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/SignerConsentActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/SignerConsentActivity.kt index 0adadfe443..7c09dee9a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/SignerConsentActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/SignerConsentActivity.kt @@ -75,6 +75,7 @@ import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.napplet_consent_allow_always import com.vitorpamplona.amethyst.commons.resources.napplet_consent_fewer_options @@ -94,7 +95,6 @@ import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_deny import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_remember import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_signing_as import com.vitorpamplona.amethyst.commons.resources.nip46_signer_messages_with -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.call.CallSessionBridge import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt index 52f37853d5..4d1d51c1ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt @@ -28,7 +28,7 @@ import android.widget.Toast import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.napplet.NappletLauncher import com.vitorpamplona.amethyst.napplet.NappletWebViewProfiles diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteNostrAppPreloader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteNostrAppPreloader.kt index c7d58eee97..8d240b33b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteNostrAppPreloader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteNostrAppPreloader.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.favorites import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryState import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/NappletFavoriteIcon.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/NappletFavoriteIcon.kt index e03d31a737..330f648f09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/NappletFavoriteIcon.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/NappletFavoriteIcon.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.browser.OmniboxInput -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.napplethost.NappletBlobCache import com.vitorpamplona.amethyst.napplethost.NappletBlobPrefetcher import com.vitorpamplona.quartz.nip01Core.core.Event 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 e72e2907a3..798b6ff4a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.model.buzz.BuzzChannelStars import com.vitorpamplona.amethyst.commons.model.buzz.BuzzHeldAttestations import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect import com.vitorpamplona.amethyst.commons.model.buzz.BuzzWorkspaces +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannelListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountRelayGroupActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountRelayGroupActions.kt index a5bb8e9a7b..33612b11ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountRelayGroupActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountRelayGroupActions.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect import com.vitorpamplona.amethyst.commons.model.buzz.WorkflowRunPayload +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupDeletions import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupMembership diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountZapActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountZapActions.kt index eb854749ea..52a145f00c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountZapActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountZapActions.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendError import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendStage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/DvmHeartbeat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/DvmHeartbeat.kt index aafb366746..97ad92855c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/DvmHeartbeat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/DvmHeartbeat.kt @@ -20,7 +20,9 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet +import com.vitorpamplona.amethyst.commons.model.nip90DVMs.DvmHeartbeatRegistry import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/GitPullRequestUpdateIndex.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/GitPullRequestUpdateIndex.kt index 476a95563e..54912b0925 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/GitPullRequestUpdateIndex.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/GitPullRequestUpdateIndex.kt @@ -20,7 +20,8 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.LocalCache.observeEvents +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache.observeEvents import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestUpdateEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/GitStatusIndex.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/GitStatusIndex.kt index a19dce396f..354eaacf66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/GitStatusIndex.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/GitStatusIndex.kt @@ -20,7 +20,8 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.LocalCache.observeEvents +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache.observeEvents import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip34Git.status.GitStatusAppliedEvent 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 cdb7bfebde..0c5fdaa0a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey class ParticipantListBuilder { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt index 75c45dcd4b..73737b3f64 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt @@ -27,11 +27,11 @@ import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46ClientStore import com.vitorpamplona.amethyst.commons.connectedApps.signers.InMemoryNostrSignerPermissionStore import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore import com.vitorpamplona.amethyst.commons.marmot.InMemoryMlsGroupStateStore +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.commons.service.pow.PoWPublishQueue import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.marmot.AndroidIngestDedupStore import com.vitorpamplona.amethyst.model.marmot.AndroidKeyPackageBundleStore import com.vitorpamplona.amethyst.model.marmot.AndroidMarmotMessageStore diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/bolt12Offers/Bolt12OfferListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/bolt12Offers/Bolt12OfferListState.kt index 9d3557a44e..3cff972cc8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/bolt12Offers/Bolt12OfferListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/bolt12Offers/Bolt12OfferListState.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.model.bolt12Offers import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListEvent import com.vitorpamplona.quartz.utils.Log diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/buzz/BuzzMembershipNotices.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/buzz/BuzzMembershipNotices.kt index d5a699e283..6665b1de53 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/buzz/BuzzMembershipNotices.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/buzz/BuzzMembershipNotices.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.model.buzz import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.buzz.ChannelClassification import com.vitorpamplona.amethyst.commons.model.buzz.MembershipNotice +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.buzz.MembershipNotificationKinds import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent import com.vitorpamplona.quartz.buzz.notifications.MemberRemovedNotificationEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/buzz/ChannelInvitesState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/buzz/ChannelInvitesState.kt index 2fce4a5ca0..c3ef6ae3dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/buzz/ChannelInvitesState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/buzz/ChannelInvitesState.kt @@ -24,8 +24,8 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.buzz.BuzzChannelInvite import com.vitorpamplona.amethyst.commons.model.buzz.BuzzChannelInvites import com.vitorpamplona.amethyst.commons.model.buzz.BuzzWorkspaces +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupListState -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent 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 aa96e11a0c..531afe8cef 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 @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.model.edits import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.edits.PrivateStorageRelayListDecryptionCache 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.signers.NostrSigner import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent 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 842eaacc9a..920e15ae5f 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 @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.localRelays +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/marmot/KeyPackageRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/marmot/KeyPackageRelayListState.kt index 87171f5311..d472599499 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/marmot/KeyPackageRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/marmot/KeyPackageRelayListState.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.model.marmot import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner 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 3bacb99eb3..4c4d211e5b 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 @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip01UserMetadata +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt index f73142939c..14b9e9385b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.OutboxRelayLoader -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerUsingRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerUsingRelay.kt index 06ebc99e82..a13d61a8ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerUsingRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerUsingRelay.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.UsingRelayUnwrapper -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt index 1c21369762..4ee9ddb634 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.OutboxRelayLoader -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt index b383dee2d4..da14281d14 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.OutboxRelayLoader -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl 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 1913ff35c6..056d0c63ae 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 @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.OutboxRelayLoader -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt index 4ca8cc4c62..b3dc8afdc3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.model.nip02FollowLists import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt index d6670436db..9f46b7ea73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.model.nip03Timestamp import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip03Timestamp.cacheVerifyOts -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder import kotlinx.coroutines.CoroutineScope 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 ca69b8295c..d606671d44 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 @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.model.nip03Timestamp import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent 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 82cfe774d0..360fd7309b 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 @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.model.nip17Dms import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache 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.signers.NostrSigner import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt index 033e6340eb..7021497479 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt @@ -23,13 +23,13 @@ package com.vitorpamplona.amethyst.model.nip30CustomEmojis import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.OwnedEmojiPack import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.signers.update diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip46Signer/Nip46ConsentBridge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip46Signer/Nip46ConsentBridge.kt index 104166e77a..b7219ff892 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip46Signer/Nip46ConsentBridge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip46Signer/Nip46ConsentBridge.kt @@ -26,10 +26,10 @@ import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46PermissionAut import com.vitorpamplona.amethyst.commons.connectedApps.signers.AppConnectResult import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerOp import com.vitorpamplona.amethyst.commons.connectedApps.signers.SignerOpGrant +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.connectedApps.consent.SignerConnectCoordinator import com.vitorpamplona.amethyst.connectedApps.consent.SignerConnectInfo import com.vitorpamplona.amethyst.connectedApps.consent.SignerConsentCoordinator -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.napplet.label import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner 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 index b9f17a8083..2252b83c4f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt @@ -22,11 +22,11 @@ package com.vitorpamplona.amethyst.model.nip47WalletConnect import com.vitorpamplona.amethyst.commons.model.INwcSignerState import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcInfoCache import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentQueryState import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/PinListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/PinListState.kt index 1670b512c9..60344802d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/PinListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/PinListState.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt index 3e86b8a136..0b70c83600 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag 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 ae61bd1b1a..438f2c4b09 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 @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache 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.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions 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 index cba4355ac2..60a3bc2307 100644 --- 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 @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache 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.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListState.kt index 9a95ffef8c..fa635297bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListState.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.model.nip51Lists.favoriteAlgoFeedsLists import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListDecryptionCache import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt index dd4355c1eb..6f8538e3d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.geohashLists.GeohashListDecryptionCache import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent import com.vitorpamplona.quartz.utils.Log 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 6c7adf8e93..92869bfbb5 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 @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import com.vitorpamplona.quartz.utils.Log 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 index d460826cc6..5c62fe7077 100644 --- 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 @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache 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.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSetsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSetsState.kt index 5686d4c38b..487aa8748b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSetsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSetsState.kt @@ -23,13 +23,13 @@ package com.vitorpamplona.amethyst.model.nip51Lists.interestSets import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.InterestSet import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt index ce6d866eef..71626d7d4e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt @@ -23,13 +23,13 @@ package com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.update diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt index 6c99af2d09..2badb31fd1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.model.nip51Lists.muteList import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList.MuteListDecryptionCache import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt index 24a563826f..d36155715f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt @@ -24,12 +24,12 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.update diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt index 53601aff70..fd2c8de163 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt @@ -24,13 +24,13 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.update 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 index 14355b05c9..20b4320f99 100644 --- 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 @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache 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.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt index 5d47fd55b6..e0d90c5310 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.model.nip51Lists.relayFeeds import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache 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.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt index 6ccc74156d..572312fba8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays import com.vitorpamplona.amethyst.commons.defaults.DefaultSearchRelayList import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache 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.signers.NostrSigner import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt index a7af1074ff..16c635d0f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache 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.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuMintDirectoryState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuMintDirectoryState.kt index 078552dd16..44aeb1e718 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuMintDirectoryState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuMintDirectoryState.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.model.nip60Cashu import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.assemblers.CashuMintDirectoryFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.assemblers.CashuMintDirectoryQueryState -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt index b67a4a0759..98a005ebdc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt @@ -28,9 +28,9 @@ import com.vitorpamplona.amethyst.commons.cashu.ops.RestoreOutcome import com.vitorpamplona.amethyst.commons.cashu.ops.SendTokenCompleted import com.vitorpamplona.amethyst.commons.cashu.ops.TokenEntry import com.vitorpamplona.amethyst.commons.cashu.ops.describeMintError +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.assemblers.cashuProofBackfillFilters import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip62Vanish/VanishRequestsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip62Vanish/VanishRequestsState.kt index 63ccacf2a8..006fee5203 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip62Vanish/VanishRequestsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip62Vanish/VanishRequestsState.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.model.nip62Vanish import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchFirst import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter 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 3b01fe1045..581906ef10 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 @@ -24,8 +24,8 @@ import com.vitorpamplona.amethyst.commons.defaults.Constants import com.vitorpamplona.amethyst.commons.defaults.relayListOrDefaultsWhenUnknown import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache 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.signers.NostrSigner import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent 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 31c7ffc1f9..e1ae825bc2 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 @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.model.nip72Communities import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip72Communities.CommunityListDecryptionCache import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent 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 9b246a787c..14d3f6e18f 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 @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.model.nip78AppSpecific import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AccountSyncedSettingsInternal -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.JsonMapper import com.vitorpamplona.quartz.nip01Core.core.awaitCreatedAtToSupersede import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip89AppHandlers/AppRecommendationsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip89AppHandlers/AppRecommendationsState.kt index 1184e18483..40ff855b42 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip89AppHandlers/AppRecommendationsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip89AppHandlers/AppRecommendationsState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip89AppHandlers +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.awaitCreatedAtToSupersede import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipA3PaymentTargets/NipA3PaymentTargetsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipA3PaymentTargets/NipA3PaymentTargetsState.kt index 5193e777ee..b2012e8794 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipA3PaymentTargets/NipA3PaymentTargetsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipA3PaymentTargets/NipA3PaymentTargetsState.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.model.nipA3PaymentTargets import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner 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 d9b66f7102..fbe2d07a80 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 @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.FeedDecryptionCaches import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavFilter @@ -31,7 +32,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.hashtag.MultiHashtag import com.vitorpamplona.amethyst.commons.model.topNavFeeds.mine.MineFeedFlow import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.NoteFeedFlow import com.vitorpamplona.amethyst.commons.model.topNavFeeds.relay.RelayFeedFlow -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.algoFeeds.FavoriteAlgoFeedsOrchestrator import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt index 3fcadf9b34..80f88da3cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.model.trustedAssertions import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListDecryptionCache import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.list.tags.ProviderTypes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/zap/RailCapability.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/zap/RailCapability.kt index 0c3b1593c8..286ded391f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/zap/RailCapability.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/zap/RailCapability.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.model.zap import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.payments.PayToRailMatcher import com.vitorpamplona.amethyst.commons.model.payments.PaymentTargetTypes -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.MIN_ONCHAIN_ZAP_SATS import com.vitorpamplona.amethyst.model.nip60Cashu.CashuWalletState import com.vitorpamplona.amethyst.service.payments.PayToAppAvailability diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt index c923b799d7..7accd6ebf2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt @@ -25,9 +25,9 @@ import android.content.Intent import android.content.res.Configuration import android.os.Bundle import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.napplet.NappletArtifactPolicy import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.napplethost.HostProfile import com.vitorpamplona.amethyst.napplethost.NappletHostActivity diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt index e53b9aceac..15527dc5c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt @@ -24,6 +24,7 @@ import android.content.Context import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.browser.OmniboxInput import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerOp +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.napplet.NappletCapability import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletRequest @@ -32,7 +33,6 @@ import com.vitorpamplona.amethyst.commons.napplet.protocol.toNarrowSignerOp import com.vitorpamplona.amethyst.connectedApps.consent.SignerConnectInfo import com.vitorpamplona.amethyst.connectedApps.consent.SignerConsentInfo import com.vitorpamplona.amethyst.favorites.BrowserIconRegistry -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kindNameFor import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey 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 0d08176f3c..cf5312996c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -25,9 +25,9 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.payments.PaymentSource import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.ui.nwc.nwcFailureDetail import com.vitorpamplona.amethyst.ui.nwc.nwcTimeoutMessage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt index fb65cb09fa..2e438afb67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt @@ -28,8 +28,8 @@ import androidx.work.WorkManager import androidx.work.WorkerParameters import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip52Calendar.appointmentView -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.resourceusage.UsageKeys import com.vitorpamplona.amethyst.ui.pluralStringRes import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/notification/CallNotifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/notification/CallNotifier.kt index 789f6ff535..cdcff12365 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/notification/CallNotifier.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/notification/CallNotifier.kt @@ -32,7 +32,7 @@ import coil3.asDrawable import coil3.request.ImageRequest import coil3.request.allowHardware import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.service.call.CallNotificationReceiver import com.vitorpamplona.amethyst.ui.call.CallActivity import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/crashreports/DisplayCrashMessages.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/crashreports/DisplayCrashMessages.kt index 7a7aae06f6..2e2dcc5267 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/crashreports/DisplayCrashMessages.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/crashreports/DisplayCrashMessages.kt @@ -37,12 +37,12 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.crashreport_found import com.vitorpamplona.amethyst.commons.resources.crashreport_found_send import com.vitorpamplona.amethyst.commons.resources.would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared import com.vitorpamplona.amethyst.commons.service.crashreports.DEV_REPORT_PUBKEY -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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 04e54c7c60..bfaade94b4 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 @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.eventCache import android.content.ComponentCallbacks2 import com.vitorpamplona.amethyst.AccountInfo +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull import com.vitorpamplona.quartz.utils.Log import java.util.concurrent.atomic.AtomicBoolean @@ -63,7 +63,7 @@ class MemoryTrimmingService( // Tier 2: real reclaim pressure — drop events from muted/blocked users, old // messages, and unobserved reactions. account.forEach { - cache.pruner.pruneHiddenEvents(it) + cache.pruner.pruneHiddenEvents(it.hiddenUsers.flow.value) cache.pruner.pruneHiddenMessages(it) } val accounts = otherAccounts.mapNotNull { decodePublicKeyAsHexOrNull(it.npub) }.toSet() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/AlwaysOnNotificationServiceManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/AlwaysOnNotificationServiceManager.kt index 30b3add1dc..c00e7c506c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/AlwaysOnNotificationServiceManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/AlwaysOnNotificationServiceManager.kt @@ -77,7 +77,7 @@ import kotlinx.coroutines.launch * Every saved writable account is kept loaded in [AccountCacheState] whenever either condition * holds, so (a) participation flags are observable and (b) GiftWraps addressed to any of them get * unwrapped by the owning account's `newNotesPreProcessor`. Without this, wraps for non-active - * accounts would sit in [com.vitorpamplona.amethyst.model.LocalCache] with no subscriber able to + * accounts would sit in [com.vitorpamplona.amethyst.commons.model.cache.LocalCache] with no subscriber able to * decrypt them. */ class AlwaysOnNotificationServiceManager( 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 f28d53b3a8..b965948513 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 @@ -32,12 +32,12 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.isMutedPublicChatMessage import com.vitorpamplona.amethyst.commons.nipACWebRtcCalls.CallManager import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryState import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.call.notification.CallNotifier import com.vitorpamplona.amethyst.service.notifications.renderers.ArticleNotification import com.vitorpamplona.amethyst.service.notifications.renderers.BadgeNotification diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationContent.kt index 9057d34006..238b20c0bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationContent.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.service.notifications import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip71Video.selectVideoTrack import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt index 41c4619596..f75661b6f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.notifications import android.content.Context import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.renderers.BuzzDmNotification import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal.NotificationFeedFilter import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationEnricher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationEnricher.kt index c5285010da..b6422968a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationEnricher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationEnricher.kt @@ -46,7 +46,7 @@ import kotlinx.coroutines.withTimeoutOrNull /** * Makes a tray notification *observable*: it renders immediately from whatever - * is already in [com.vitorpamplona.amethyst.model.LocalCache], then — if the + * is already in [com.vitorpamplona.amethyst.commons.model.cache.LocalCache], then — if the * involved users' names/pictures or the involved notes' content/media haven't * loaded yet — opens a bounded relay window, subscribes to those users and * notes, and re-renders the same notification (replacing it in place) as the diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationReplyReceiver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationReplyReceiver.kt index 26b2930f02..48c0e20cab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationReplyReceiver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationReplyReceiver.kt @@ -28,7 +28,7 @@ import androidx.core.app.RemoteInput import androidx.core.content.ContextCompat import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.cancelAndPrune import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.cancelChildlessGroupSummaries diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt index 5f0980204c..ce8642182b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt @@ -26,7 +26,7 @@ import android.content.Context.RECEIVER_EXPORTED import android.content.Intent import android.content.IntentFilter import android.os.Build -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineExceptionHandler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PushWrapDecryptor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PushWrapDecryptor.kt index bd6b3735ca..17964dbecd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PushWrapDecryptor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PushWrapDecryptor.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.notifications import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ArticleNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ArticleNotification.kt index 9e886a3e30..641b2970f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ArticleNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ArticleNotification.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.notifications.renderers import android.content.Context import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationContent import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/BadgeNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/BadgeNotification.kt index ed953f73bc..47f932ee0e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/BadgeNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/BadgeNotification.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.notifications.renderers import android.content.Context import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/BuzzDmNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/BuzzDmNotification.kt index d0093697ee..eaf4a66d09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/BuzzDmNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/BuzzDmNotification.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.service.notifications.renderers import android.content.Context import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ChessNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ChessNotification.kt index f3aa6bf7ab..f29b7e028a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ChessNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ChessNotification.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.service.notifications.renderers import android.content.Context import androidx.annotation.StringRes import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/CodeNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/CodeNotification.kt index 086f230ac7..cad5666a60 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/CodeNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/CodeNotification.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.notifications.renderers import android.content.Context import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationContent import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/DirectMessageNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/DirectMessageNotification.kt index e1b7319b21..4556e575bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/DirectMessageNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/DirectMessageNotification.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.service.notifications.renderers import android.content.Context +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationContent import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/GroupMessageNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/GroupMessageNotification.kt index ebcc0d834b..1f280a0c19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/GroupMessageNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/GroupMessageNotification.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.notifications.renderers import android.content.Context import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/MediaNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/MediaNotification.kt index c073fb605d..3308e2f0f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/MediaNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/MediaNotification.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.notifications.renderers import android.content.Context import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationContent import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/MentionNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/MentionNotification.kt index a996247dc6..269d7d8774 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/MentionNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/MentionNotification.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service.notifications.renderers import android.content.Context import androidx.annotation.StringRes import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationContent import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ReactionNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ReactionNotification.kt index 99cd834adb..7b766c07f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ReactionNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ReactionNotification.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.notifications.renderers import android.content.Context import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationContent import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ReplyNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ReplyNotification.kt index 752cf2915a..f177dbaeb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ReplyNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ReplyNotification.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service.notifications.renderers import android.content.Context import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationContent import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/RepostNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/RepostNotification.kt index d5b5676918..ab1bff0521 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/RepostNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/RepostNotification.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.notifications.renderers import android.content.Context import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationContent import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ZapNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ZapNotification.kt index b47fe6b4ea..fb1968df12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ZapNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ZapNotification.kt @@ -24,8 +24,8 @@ import android.content.Context import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationContent import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher 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 8a6e418b7e..534c130de9 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 @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.EventCollector import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayInsertConfirmationCollector diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt index efeaa9ff49..7813bcb039 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt @@ -65,6 +65,7 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.auth.RelayAuthPrompt import com.vitorpamplona.amethyst.commons.relayClient.auth.UserAuthChoice import com.vitorpamplona.amethyst.commons.relayauth.AuthPurpose @@ -87,7 +88,6 @@ import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_read_venue import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_send_dm import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_thread import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_thread_with -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo 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 18e9bf5518..fd85b037db 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 @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.apps.recommendations.ProfileAppRecommendationsFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.articles.ArticlesFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.assemblers.CashuMintDirectoryFilterAssembler @@ -69,7 +70,6 @@ import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssem import com.vitorpamplona.amethyst.commons.relayClient.video.VideoFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.wallet.OnchainZapsFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.workouts.WorkoutsFilterAssembler -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.cachedDvmAnnouncements import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountForegroundFilterAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountForegroundFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountForegroundFilterAssembler.kt index a6d9109bff..90491de091 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountForegroundFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountForegroundFilterAssembler.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.AccountFollowsLoaderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.AccountNotificationsEoseFromRandomRelaysManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt index 45b306a60a..20558e89bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.IEoseManager import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose @@ -28,7 +29,6 @@ import com.vitorpamplona.amethyst.commons.relays.EOSEAccountFast import com.vitorpamplona.amethyst.commons.service.BundledUpdate import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountUiQueryState import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt index 9cd1fb9fa5..b29560e16b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt @@ -24,10 +24,10 @@ import com.vitorpamplona.amethyst.commons.defaults.Constants import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList import com.vitorpamplona.amethyst.commons.defaults.DefaultSearchRelayList import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.user.pickRelaysToLoadUsers import com.vitorpamplona.amethyst.commons.relays.EOSEAccountFast import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/DisplayResourceUsageAlert.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/DisplayResourceUsageAlert.kt index 64612483e8..364e6a6a62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/DisplayResourceUsageAlert.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/DisplayResourceUsageAlert.kt @@ -34,13 +34,13 @@ import androidx.compose.ui.res.pluralStringResource import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.collectMemorySnapshot +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.resource_usage_alert_not_now import com.vitorpamplona.amethyst.commons.resources.resource_usage_alert_opt_out import com.vitorpamplona.amethyst.commons.resources.resource_usage_alert_send import com.vitorpamplona.amethyst.commons.resources.resource_usage_alert_title import com.vitorpamplona.amethyst.commons.service.crashreports.DEV_REPORT_PUBKEY -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/workouts/health/PublishedWorkouts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/workouts/health/PublishedWorkouts.kt index 7f0947f50a..9fa7752646 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/workouts/health/PublishedWorkouts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/workouts/health/PublishedWorkouts.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.workouts.health import com.vitorpamplona.amethyst.commons.fitness.DetectedWorkout import com.vitorpamplona.amethyst.commons.fitness.toDetectedWorkout -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.experimental.fitness.workout.WorkoutRecordEvent import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import kotlinx.coroutines.flow.Flow 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 157e9ea2a8..ac76fd1d14 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -28,10 +28,10 @@ import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.actions.ConcordActions +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.richtext.RichTextParser 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.notifications.NotificationRelayService import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt index 506a8d338f..f6c87b44b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt @@ -25,7 +25,7 @@ import androidx.compose.foundation.text.input.TextFieldBuffer import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.style.TextDecoration -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull import kotlin.coroutines.cancellation.CancellationException 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 ae91c21ad6..410176cef5 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 @@ -60,13 +60,13 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.concord_invite_naddr_label +import com.vitorpamplona.amethyst.commons.util.njumpLink import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNickname 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.amethyst.ui.stringRes import com.vitorpamplona.quartz.concord.cord05Invites.bundle.ConcordInviteBundleEvent 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 23a573b4b4..7786cf96fe 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 @@ -68,6 +68,7 @@ import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.richtext.Base64Segment import com.vitorpamplona.amethyst.commons.richtext.BechSegment import com.vitorpamplona.amethyst.commons.richtext.BlossomUriSegment @@ -103,7 +104,6 @@ import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.commons.ui.richtext.HashtagIcon import com.vitorpamplona.amethyst.commons.ui.richtext.checkForHashtagWithIcon import com.vitorpamplona.amethyst.commons.ui.state.produceCachedState -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNickname 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 424faa43f7..c8b48170fa 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,9 +41,9 @@ import com.halilibo.richtext.markdown.BasicMarkdown import com.halilibo.richtext.ui.material3.RichText import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.preview.UrlInfoItem import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.UrlCachedPreviewer 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/components/namecoin/NamecoinResolutionRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/namecoin/NamecoinResolutionRow.kt index fd0458eb5c..1f306f5b46 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/namecoin/NamecoinResolutionRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/namecoin/NamecoinResolutionRow.kt @@ -67,7 +67,7 @@ import kotlinx.coroutines.withContext * produces the same diagnostic string regardless of which surface * triggered the lookup. Callers are expected to handle * [NamecoinResolveOutcome.Success] separately (it needs a [User] - * lookup through [com.vitorpamplona.amethyst.model.LocalCache], which + * lookup through [com.vitorpamplona.amethyst.commons.model.cache.LocalCache], which * this helper has no access to). */ fun mapOutcomeToResolveState(outcome: NamecoinResolveOutcome): NamecoinResolveState = 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 ddd0969d05..42e7e376f8 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 @@ -43,10 +43,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.error_dialog_talk_to_user import com.vitorpamplona.amethyst.commons.resources.error_dialog_talk_to_user_name -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage 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 d23ca75a98..1e7f675229 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 @@ -31,9 +31,9 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.error_dialog_button_ok -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav 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/navigation/bottombars/GroupBottomBarEntries.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/GroupBottomBarEntries.kt index 84dc39f278..eb47325310 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/GroupBottomBarEntries.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/GroupBottomBarEntries.kt @@ -35,9 +35,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.ChannelState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt index 8aee5ace4f..0c8f21ac26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt @@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.account_switch_active_account import com.vitorpamplona.amethyst.commons.resources.account_switch_add_account_btn @@ -67,7 +68,6 @@ import com.vitorpamplona.amethyst.commons.resources.are_you_sure_you_want_to_log import com.vitorpamplona.amethyst.commons.resources.log_out import com.vitorpamplona.amethyst.commons.resources.profile_image import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostStatus -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage 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 ffd72bd695..e3e94d74c9 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 @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.navigation.routes import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom @@ -29,7 +30,6 @@ import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChann import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent 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 8c13a982ad..be7ac53168 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 @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.isCommunityDefinition import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel @@ -81,7 +82,6 @@ import com.vitorpamplona.amethyst.commons.ui.note.RenderCashuMint import com.vitorpamplona.amethyst.commons.ui.note.RenderFedimint import com.vitorpamplona.amethyst.commons.ui.note.RenderMintRecommendation import com.vitorpamplona.amethyst.commons.ui.state.produceCachedStateAsync -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent 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 a5d615f062..dbee780385 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,6 +89,7 @@ import com.vitorpamplona.amethyst.commons.resources.quick_action_request_deletio import com.vitorpamplona.amethyst.commons.resources.quick_action_request_deletion_alert_title import com.vitorpamplona.amethyst.commons.resources.quick_action_unfollow import com.vitorpamplona.amethyst.commons.resources.report_dialog_blocking_a_user +import com.vitorpamplona.amethyst.commons.util.njumpLink import com.vitorpamplona.amethyst.ui.components.util.setText import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo @@ -123,10 +124,6 @@ val externalLinkForUser = { user: User -> njumpLink(user.toNProfile()) } -val njumpLink = { nip19BechAddress: String -> - "https://njump.to/$nip19BechAddress" -} - val nipLink = { nipNumber: String -> "https://nostrhub.io/$nipNumber" } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/OnchainZapGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/OnchainZapGallery.kt index 6d7d8e6cf5..7da1f4bb3e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/OnchainZapGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/OnchainZapGallery.kt @@ -40,7 +40,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.OnchainZapEntry import com.vitorpamplona.amethyst.commons.model.OnchainZapStatus -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps 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/note/ZapPollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt index 813ea3325e..9d807b8ca0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt @@ -77,8 +77,8 @@ import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer 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 0a0812235e..155618b690 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 @@ -49,10 +49,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.user_suggestion_in_this_chat -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture 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 8ec6ec354d..8e73d90787 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 @@ -28,8 +28,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextColor -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt index c0fad800f6..3e50c215fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt @@ -30,9 +30,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.location.CachedReversedGeoLocations import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.note.FirstUserInfoRow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/StaleRelayHint.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/StaleRelayHint.kt index 54a663c777..c2f718c260 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/StaleRelayHint.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/StaleRelayHint.kt @@ -37,10 +37,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.stale_relay_hint_icon_description import com.vitorpamplona.amethyst.commons.resources.stale_relay_hint_label -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.warningColor import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent 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 f96640d657..1a52f53068 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 @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay @@ -46,7 +47,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayCommunityScope.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayCommunityScope.kt index 1c524ef01b..0657160213 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayCommunityScope.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayCommunityScope.kt @@ -26,7 +26,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.types.ShortCommunityHeaderNoActions diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityCardPreviews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityCardPreviews.kt index db71e5b4f8..392938ef65 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityCardPreviews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityCardPreviews.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt index b724aac13f..921f49f624 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt @@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.attestation_invalid @@ -65,7 +66,6 @@ import com.vitorpamplona.amethyst.commons.resources.attestation_valid import com.vitorpamplona.amethyst.commons.resources.attestation_valid_from import com.vitorpamplona.amethyst.commons.resources.attestation_valid_to import com.vitorpamplona.amethyst.commons.resources.attestor_recommendation_for_kinds -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav 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 8178f9ebd6..1aff4c975c 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 @@ -62,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.accept_badge import com.vitorpamplona.amethyst.commons.resources.award_granted_to @@ -72,7 +73,6 @@ import com.vitorpamplona.amethyst.commons.resources.badge_awardees_label import com.vitorpamplona.amethyst.commons.resources.badge_untitled import com.vitorpamplona.amethyst.commons.resources.reject_badge import com.vitorpamplona.amethyst.commons.resources.unaccept_badge -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarEvent.kt index d9b6ecf0b7..f61ad417be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarEvent.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.components.MyAsyncImage 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/note/types/CalendarRsvpRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRow.kt index 052f878419..3710f4b89a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRow.kt @@ -38,11 +38,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.calendar_rsvp_going import com.vitorpamplona.amethyst.commons.resources.calendar_rsvp_maybe import com.vitorpamplona.amethyst.commons.resources.calendar_rsvp_not_going -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelInvite.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelInvite.kt index 84b19e5cf1..d9adeefd45 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelInvite.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelInvite.kt @@ -55,6 +55,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.channel_invite_added_you @@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.commons.resources.channel_invite_leave import com.vitorpamplona.amethyst.commons.resources.channel_invite_unknown_actor import com.vitorpamplona.amethyst.commons.resources.relay_group_badge_invite_only import com.vitorpamplona.amethyst.commons.resources.relay_group_badge_private -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.buzz.toMembershipNotice import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ContactList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ContactList.kt index 9d08b1f019..e9b9f36623 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ContactList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ContactList.kt @@ -34,9 +34,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.contact_list_containing_label -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route 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 76accf4137..95ecf48a1d 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 @@ -43,10 +43,10 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.follow_list_item_label -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMap import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/GitStatusActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/GitStatusActions.kt index aac946bbb1..3f3303add9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/GitStatusActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/GitStatusActions.kt @@ -40,12 +40,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.git_status_close import com.vitorpamplona.amethyst.commons.resources.git_status_mark_merged import com.vitorpamplona.amethyst.commons.resources.git_status_reopen import com.vitorpamplona.amethyst.model.GitStatusIndex -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.Address 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 884f80da98..cae9fc9ebc 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 @@ -48,10 +48,10 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.live_stream_has_ended import com.vitorpamplona.amethyst.commons.resources.live_stream_is_offline -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt index a5bae53c6f..968a64bfd6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt @@ -402,7 +402,7 @@ fun RenderMeetingRoomPresence( val user = remember(event.pubKey) { - com.vitorpamplona.amethyst.model.LocalCache + com.vitorpamplona.amethyst.commons.model.cache.LocalCache .getOrCreateUser(event.pubKey) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt index 0f780eda2a..155532ee7f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt @@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.music_playlist_collaborative @@ -68,7 +69,6 @@ import com.vitorpamplona.amethyst.commons.resources.music_playlist_edit_action import com.vitorpamplona.amethyst.commons.resources.music_playlist_loading_track import com.vitorpamplona.amethyst.commons.resources.music_playlist_private import com.vitorpamplona.amethyst.commons.resources.music_playlist_unknown_track -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.MyAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt index ec1ad4d5a0..dc7a631390 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt @@ -56,8 +56,8 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController import com.vitorpamplona.amethyst.service.playback.composable.LoadThumbAndThenVideoView import com.vitorpamplona.amethyst.service.playback.composable.PauseControllerWhenInBackground diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt index ec2c2622bc..1589e21bc1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt @@ -39,9 +39,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.kinds -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav 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/note/types/OnchainZapEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/OnchainZapEvent.kt index 54f43448d9..573d3d2ba5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/OnchainZapEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/OnchainZapEvent.kt @@ -70,7 +70,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.components.util.setText import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserPicture 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 16a3e0865a..620431d218 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 @@ -74,13 +74,13 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip88Polls.PollResponsesCache import com.vitorpamplona.amethyst.commons.model.nip88Polls.TallyResults import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.poll_submit import com.vitorpamplona.amethyst.commons.resources.poll_view_results -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ProfileCardPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ProfileCardPreview.kt index 4577647722..33095920a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ProfileCardPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ProfileCardPreview.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav 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/types/RelayGroupMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayGroupMessage.kt index 8b94592225..ea9b59115c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayGroupMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayGroupMessage.kt @@ -43,9 +43,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName 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/note/types/SoftwareApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt index f6ce6ea00f..752dcabce6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt @@ -64,6 +64,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.nip82_by_author @@ -73,7 +74,6 @@ import com.vitorpamplona.amethyst.commons.resources.nip82_version_label import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.commons.util.prettyMime -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.MediaAspectRatioCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.ZoomableContentView 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 ea5f987dc2..cfae407898 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 @@ -35,11 +35,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.replyingDirectlyTo import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable import com.vitorpamplona.amethyst.commons.ui.note.ReplyToLabel -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.components.SensitivityWarning 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/note/types/Torrent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt index fd5c151546..9f03ba2e80 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 @@ -50,8 +50,8 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.components.ShowMoreButton 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/note/types/TorrentComment.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt index d375f1cec2..61429d4c65 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 @@ -45,11 +45,11 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.torrent_file import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt index ebae382e4c..7ffd6988d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt @@ -42,11 +42,11 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.note.ActivityAmountRow import com.vitorpamplona.amethyst.commons.ui.note.ActivityBadge import com.vitorpamplona.amethyst.commons.ui.note.ActivityCardFrame import com.vitorpamplona.amethyst.commons.ui.note.ActivityHeaderRow -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.CrossfadeToDisplayComment diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapPoll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapPoll.kt index 0706b83d8b..31d369a02b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapPoll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapPoll.kt @@ -34,9 +34,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.replyingDirectlyTo import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.components.SensitivityWarning 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/AndroidFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AndroidFeedViewModel.kt index 0e276722ea..461aaf220c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AndroidFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AndroidFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.viewmodels.FeedViewModel -import com.vitorpamplona.amethyst.model.LocalCache /** * Android-specific FeedViewModel base class that provides LocalCache as the cache provider. 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 11a03b2d60..d0ee9ac934 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 @@ -28,8 +28,8 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.feeds.InvalidatableContent import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.BundledUpdate -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.utils.Log diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index 6431de9af5..a6aea82708 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -24,9 +24,9 @@ import android.content.ComponentCallbacks2 import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState 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 c53ba18afc..6f2517432b 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.commons.model.Dao import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel @@ -72,7 +73,6 @@ import com.vitorpamplona.amethyst.commons.ui.state.GenericBaseCacheAsync import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder @@ -335,7 +335,7 @@ class AccountViewModel( /** * A fresh, relay-authenticated [INostrClient] whose received events do NOT - * land in the production [com.vitorpamplona.amethyst.model.LocalCache] — for + * land in the production [com.vitorpamplona.amethyst.commons.model.cache.LocalCache] — for * crawls (Event Sync, Cashu wallet discovery). The caller must `close()` it. */ fun buildCrawlClient(): INostrClient { 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 a9b042bb65..1afbd547a4 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 @@ -22,12 +22,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.chatMessageMarksRoomAsRead import com.vitorpamplona.amethyst.commons.model.privateChatLastReadRoute import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList import com.vitorpamplona.amethyst.commons.nipACWebRtcCalls.CallManager import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.marmot.GroupEventResult import com.vitorpamplona.quartz.marmot.MarmotInboundProcessor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/ProfileAppRecommendationsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/ProfileAppRecommendationsScreen.kt index e264d97529..858553b13c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/ProfileAppRecommendationsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/ProfileAppRecommendationsScreen.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.app_definition_handles @@ -66,7 +67,6 @@ import com.vitorpamplona.amethyst.commons.resources.profile_app_recommendations_ import com.vitorpamplona.amethyst.commons.resources.profile_app_recommendations_search import com.vitorpamplona.amethyst.commons.resources.profile_app_recommendations_search_empty import com.vitorpamplona.amethyst.commons.resources.select_list_to_filter -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/dal/ArticlesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/dal/ArticlesFeedFilter.kt index 305e69cb6c..5ec4bd4976 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/dal/ArticlesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/dal/ArticlesFeedFilter.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/award/AwardBadgeViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/award/AwardBadgeViewModel.kt index 4b738c3cad..85a86365c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/award/AwardBadgeViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/award/AwardBadgeViewModel.kt @@ -26,8 +26,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/dal/BadgesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/dal/BadgesFeedFilter.kt index 530d9ec0c1..9f650a9a2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/dal/BadgesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/dal/BadgesFeedFilter.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/profile/ProfileBadgesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/profile/ProfileBadgesScreen.kt index cf9883c149..6ff1484bfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/profile/ProfileBadgesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/profile/ProfileBadgesScreen.kt @@ -52,11 +52,11 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.badge_untitled import com.vitorpamplona.amethyst.commons.resources.profile_badges_description import com.vitorpamplona.amethyst.commons.resources.profile_badges_empty -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentAttestationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentAttestationScreen.kt index 64798d1c27..14b1834f8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentAttestationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentAttestationScreen.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.buzz.BuzzHeldAttestations +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.buzz_attest_after_label import com.vitorpamplona.amethyst.commons.resources.buzz_attest_agent_label @@ -87,7 +88,6 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_attest_remove import com.vitorpamplona.amethyst.commons.resources.buzz_attest_signed_title import com.vitorpamplona.amethyst.commons.resources.buzz_attest_topbar import com.vitorpamplona.amethyst.commons.resources.buzz_attest_warning -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.components.util.setText import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentConsoleViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentConsoleViewModel.kt index 771037b0ce..11c9d91d79 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentConsoleViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentConsoleViewModel.kt @@ -26,10 +26,10 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.model.buzz.AgentFleetAggregator import com.vitorpamplona.amethyst.commons.model.buzz.AgentFleetMetrics import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthDecision import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.buzz.amTurnMetrics.AgentTurnMetricEvent import com.vitorpamplona.quartz.buzz.amTurnMetrics.AgentTurnMetricPayload import com.vitorpamplona.quartz.buzz.aoObserver.ObserverFrameEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentPersonaEditViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentPersonaEditViewModel.kt index 7995f73d02..e3e4ed39d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentPersonaEditViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentPersonaEditViewModel.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.buzz.apPersonas.PersonaContent import com.vitorpamplona.quartz.buzz.apPersonas.PersonaEvent import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzAddPeopleDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzAddPeopleDialog.kt index 86fa2e6c41..d0a8a8e1ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzAddPeopleDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzAddPeopleDialog.kt @@ -63,7 +63,7 @@ import androidx.compose.runtime.LaunchedEffect as ComposeLaunchedEffect * [UserSuggestionState] engine the @-mention typeahead uses — over the local cache, the relays * (NIP-50) and NIP-05 identifiers, plus a pasted npub/nprofile. * - * It used to search only [com.vitorpamplona.amethyst.model.LocalCache], so anyone the device had + * It used to search only [com.vitorpamplona.amethyst.commons.model.cache.LocalCache], so anyone the device had * never seen simply had no result and the only way through was to paste a raw hex key — which is * what the field's own hint told you to do. Searching the relays is what makes finding a person by * name work at all here. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzCanvasScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzCanvasScreen.kt index 9b380f4366..f6958a8b9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzCanvasScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzCanvasScreen.kt @@ -55,13 +55,13 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.buzz.BuzzWorkspaceStates +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.buzz_canvas_body_label import com.vitorpamplona.amethyst.commons.resources.buzz_canvas_edit import com.vitorpamplona.amethyst.commons.resources.buzz_canvas_empty import com.vitorpamplona.amethyst.commons.resources.buzz_canvas_save import com.vitorpamplona.amethyst.commons.resources.buzz_canvas_title -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmDiscovery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmDiscovery.kt index 80b0da3237..36eb1dfd56 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmDiscovery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmDiscovery.kt @@ -25,9 +25,9 @@ import androidx.compose.runtime.LaunchedEffect import com.vitorpamplona.amethyst.commons.model.buzz.BuzzChannelInvites import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmChannels import com.vitorpamplona.amethyst.commons.model.buzz.ChannelClassification +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.RELAY_GROUP_METADATA_KINDS import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.buzz.buzzChannelTypes import com.vitorpamplona.amethyst.model.buzz.classifyBuzzChannel import com.vitorpamplona.amethyst.model.buzz.membershipNoticeFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt index bf8e3e347e..9ade640b48 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt @@ -68,6 +68,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.buzz_dm_add_hint import com.vitorpamplona.amethyst.commons.resources.buzz_dm_add_member @@ -79,7 +80,6 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_dm_just_you import com.vitorpamplona.amethyst.commons.resources.buzz_dm_more import com.vitorpamplona.amethyst.commons.resources.buzz_dm_new import com.vitorpamplona.amethyst.commons.resources.buzz_dm_title -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListViewModel.kt index 4fd4f3509f..50eadb9aa2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListViewModel.kt @@ -27,11 +27,11 @@ import com.vitorpamplona.amethyst.commons.model.buzz.BuzzChannelInvites import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmChannels import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmRegistry import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.RELAY_GROUP_METADATA_KINDS import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthDecision import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.buzz.membershipNoticeFilter import com.vitorpamplona.amethyst.model.buzz.membershipNotices import com.vitorpamplona.quartz.buzz.dvDmVisibility.DmVisibilityEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzForumThreadScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzForumThreadScreen.kt index c35ab55477..18fe0cc7cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzForumThreadScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzForumThreadScreen.kt @@ -48,10 +48,10 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.buzz_forum_no_replies import com.vitorpamplona.amethyst.commons.resources.buzz_forum_thread_title -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt index b595e3d23c..7af8efcdf5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt @@ -51,10 +51,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.relay_group_no_messages_yet -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.note.timeAgo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzNewDmScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzNewDmScreen.kt index d77a1d45bf..2c0c4f0c3c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzNewDmScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzNewDmScreen.kt @@ -62,6 +62,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.buzz_dm_add_hint import com.vitorpamplona.amethyst.commons.resources.buzz_dm_new @@ -70,7 +71,6 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_dm_recipients import com.vitorpamplona.amethyst.commons.resources.buzz_dm_remove import com.vitorpamplona.amethyst.commons.resources.buzz_dm_start import com.vitorpamplona.amethyst.commons.resources.buzz_dm_workspace -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzNewDmViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzNewDmViewModel.kt index abd18633d2..56dd1135d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzNewDmViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzNewDmViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.buzz.dm.DmOpenEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.isValid diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzRelayImportViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzRelayImportViewModel.kt index 2498256719..40b883d263 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzRelayImportViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzRelayImportViewModel.kt @@ -22,11 +22,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupDeletions import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.RELAY_GROUP_METADATA_KINDS import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthDecision import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent import com.vitorpamplona.quartz.buzz.stream.SystemMessageEvent import com.vitorpamplona.quartz.buzz.workspace.isBuzzDm diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt index e96ef8220b..0b7cb6ab70 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt @@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip52Calendar.IcsExport import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.calendar_collection_count @@ -59,7 +60,6 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_empty_collections_s import com.vitorpamplona.amethyst.commons.resources.calendar_empty_collections_title import com.vitorpamplona.amethyst.commons.resources.calendar_export_event import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarFilterChip.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarFilterChip.kt index 2ae295b81f..a8be08ea3e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarFilterChip.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarFilterChip.kt @@ -47,12 +47,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.calendar_filter_all import com.vitorpamplona.amethyst.commons.resources.calendar_filter_no_calendars import com.vitorpamplona.amethyst.commons.resources.calendar_filter_sheet_title import com.vitorpamplona.amethyst.commons.resources.calendar_untitled -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt index be0fdf4c00..62fc41a72a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt @@ -25,8 +25,8 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventViewModel.kt index 0d4c791244..06186eaed6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventViewModel.kt @@ -24,9 +24,9 @@ import android.content.Context import android.net.Uri import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip52Calendar.parseIsoDateToUnixSeconds import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadingState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarAppointmentsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarAppointmentsFeedFilter.kt index 95c026daf2..11a96347d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarAppointmentsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarAppointmentsFeedFilter.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip52Calendar.upcomingFirstCalendarOrder import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarCollectionsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarCollectionsFeedFilter.kt index 38079db31d..c26935be56 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarCollectionsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarCollectionsFeedFilter.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt index a0a2f6acea..6b16db9dcd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt @@ -43,11 +43,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.calendar_add_to_calendar_none import com.vitorpamplona.amethyst.commons.resources.calendar_add_to_calendar_title import com.vitorpamplona.amethyst.commons.resources.calendar_untitled -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt index 85f5d0eb9c..4a37507944 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt @@ -62,6 +62,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip52Calendar.IcsExport import com.vitorpamplona.amethyst.commons.model.nip52Calendar.appointmentView import com.vitorpamplona.amethyst.commons.resources.Res @@ -83,7 +84,6 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_share_nostr import com.vitorpamplona.amethyst.commons.resources.calendar_share_nostr_title import com.vitorpamplona.amethyst.commons.resources.calendar_untitled import com.vitorpamplona.amethyst.commons.resources.route_calendar_event_detail -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupFeedViewModel.kt index 29bc3b2e5c..e5d2b4f79d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupFeedViewModel.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.commons.feeds.MarmotGroupFeedFilter +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.viewmodels.ListChangeFeedViewModel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey class MarmotGroupFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt index 81b3eaaad5..4f9a9a50db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt @@ -56,12 +56,12 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.chat_minichat_title import com.vitorpamplona.amethyst.commons.resources.chat_send_image_title import com.vitorpamplona.amethyst.commons.resources.concord_open_channel -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt index 2d205233e6..476dc17068 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.commons.feeds.ChatroomFeedFilter +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.viewmodels.ListChangeFeedViewModel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey class ChatroomFeedViewModel( 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 2874a8914c..c203189fd2 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,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl 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 c994cc4a63..e05c96bab3 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 @@ -47,11 +47,11 @@ 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.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.messages_new_message_subject_caption import com.vitorpamplona.amethyst.commons.resources.messages_new_subject_message import com.vitorpamplona.amethyst.commons.resources.messages_new_subject_message_placeholder -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.PostButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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 863e3ff8ef..ba43c03c14 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 @@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.ui.text.currentWord @@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger 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 3879e75a3d..91e5118d54 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 @@ -52,13 +52,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.recipient_missing_dm_relays import com.vitorpamplona.amethyst.commons.resources.retry_without_encryption import com.vitorpamplona.amethyst.commons.resources.upload_without_encryption_warning import com.vitorpamplona.amethyst.commons.ui.text.onUiThread -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation 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 531dec2695..f72733a80d 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 @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.quartz.nip01Core.tags.references.references diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt index 57dffc1084..16fd5f91c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt @@ -66,8 +66,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.concord.ConcordCommunitySession -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.components.util.setText import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt index fd64e9dfff..b5bfc9573c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.concord.ConcordCommunitySession import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.resources.Res @@ -66,7 +67,6 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachSentinels import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachState -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt index d9c767b3d4..aa4f38cb7f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt @@ -59,11 +59,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.concord_home_empty import com.vitorpamplona.amethyst.commons.resources.concord_show_all_channels import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordUnread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordUnread.kt index b0e5ac00d2..93bca99702 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordUnread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordUnread.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.conco import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId import com.vitorpamplona.quartz.nip22Comments.CommentEvent import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistoryFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistoryFilterAssembler.kt index 68b107f5b6..b8a7c920f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistoryFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistoryFilterAssembler.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager @@ -29,7 +30,6 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId import com.vitorpamplona.quartz.concord.envelope.ConcordStreamEnvelope import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt index e1c90edd2f..ecd6dd4667 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt @@ -30,13 +30,13 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.viewmodels.ReplyMode import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt index 35d1d2d1cd..ce9fc17080 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.viewmodels.ListChangeFeedViewModel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache class ChannelFeedViewModel( val channel: Channel, 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 122b3e3f67..e8f7a146a7 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,8 +32,8 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.util.njumpLink 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 import com.vitorpamplona.amethyst.ui.theme.Size20Modifier 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 a4cf89ee83..93209faf56 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 @@ -31,9 +31,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt index 370396d3c2..13841d8d6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt @@ -41,13 +41,13 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.created_at import com.vitorpamplona.amethyst.commons.resources.groups_no_descriptor import com.vitorpamplona.amethyst.commons.resources.owner -import com.vitorpamplona.amethyst.model.LocalCache 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/header/ShortLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt index 2a48d10c89..3f4bb4a995 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt @@ -37,8 +37,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel -import com.vitorpamplona.amethyst.model.LocalCache 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/relayGroup/RelayGroupChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt index 8cce749dab..97252e2d54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt @@ -70,6 +70,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.buzz.BuzzCommunityMembership import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupDeletions import com.vitorpamplona.amethyst.commons.resources.Res @@ -88,7 +89,6 @@ import com.vitorpamplona.amethyst.commons.resources.relay_tor_clearnet_action import com.vitorpamplona.amethyst.commons.resources.relay_tor_clearnet_body import com.vitorpamplona.amethyst.commons.resources.relay_tor_clearnet_title import com.vitorpamplona.amethyst.commons.util.sortedBySnapshot -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip11RelayInfo.isRelaySignedRelayGroup import com.vitorpamplona.amethyst.model.nip11RelayInfo.looksLikeNonNip29Relay import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt index 9adb5445f8..d0ce191cda 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt @@ -64,6 +64,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.relay_group_parent_desc @@ -76,7 +77,6 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_parent_search import com.vitorpamplona.amethyst.commons.resources.relay_group_parent_top_level_option import com.vitorpamplona.amethyst.commons.resources.relay_group_section_structure import com.vitorpamplona.amethyst.commons.util.sortedBySnapshot -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip11RelayInfo.isRelaySignedRelayGroup import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt index c620ac9670..ccba211bb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt @@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmRegistry import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect import com.vitorpamplona.amethyst.commons.model.buzz.BuzzWorkspaceStates +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupMembership import com.vitorpamplona.amethyst.commons.resources.Res @@ -70,14 +71,13 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_role_admin import com.vitorpamplona.amethyst.commons.resources.relay_group_role_moderator import com.vitorpamplona.amethyst.commons.resources.relay_group_threads_title import com.vitorpamplona.amethyst.commons.search.SearchSeed -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.util.njumpLink import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.topbars.SearchTopBarAction import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton -import com.vitorpamplona.amethyst.ui.note.njumpLink import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.buzz.workspace.buzzParticipants diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupUnread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupUnread.kt index f3064cafc6..37105e8a5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupUnread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupUnread.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.isMinichatReply import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/RelayGroupDiscoveryFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/RelayGroupDiscoveryFeedFilter.kt index 4820b9f746..afcb987b7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/RelayGroupDiscoveryFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/RelayGroupDiscoveryFeedFilter.kt @@ -22,12 +22,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relay import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.GroupDiscoveryConstraint import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip11RelayInfo.isRelaySignedRelayGroup import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenChatHistoryFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenChatHistoryFilterAssembler.kt index f2fa2c462c..0ba237caf7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenChatHistoryFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenChatHistoryFilterAssembler.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupHistoryFilters import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager @@ -28,7 +29,6 @@ import com.vitorpamplona.amethyst.commons.relayClient.paging.BackwardRelayPager import com.vitorpamplona.amethyst.commons.relayClient.paging.PagingStatus import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsHistoryFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsHistoryFilterAssembler.kt index c8173a363f..738a50c699 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsHistoryFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsHistoryFilterAssembler.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupThreadsHistoryFilters import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager @@ -28,7 +29,6 @@ import com.vitorpamplona.amethyst.commons.relayClient.paging.BackwardRelayPager import com.vitorpamplona.amethyst.commons.relayClient.paging.PagingStatus import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoverySubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoverySubAssembler.kt index 4e9e31a924..8d0897ae01 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoverySubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoverySubAssembler.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet @@ -29,7 +30,6 @@ import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.relayGr import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.scopedTo import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter 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 baab2e6279..dead22568f 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 @@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel @@ -54,7 +55,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.viewmodels.ReplyMode import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation 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 a2859f96ce..010d604ed3 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 @@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.chatMessageMarksRoomAsRead import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel @@ -79,7 +80,6 @@ import com.vitorpamplona.amethyst.commons.resources.pinned_to_top import com.vitorpamplona.amethyst.commons.resources.referenced_event_not_found import com.vitorpamplona.amethyst.commons.resources.relay_group_no_messages_yet import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.buzz.toMembershipNotice import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel 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 ea593a5005..950fd43ec0 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 @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.concord.ConcordViewMode @@ -31,7 +32,6 @@ import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupViewM import com.vitorpamplona.amethyst.commons.model.publicChatChannelIdOf import com.vitorpamplona.amethyst.commons.util.replace import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.isConcordTimelineMessage import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.isRelayGroupTimelineMessage 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 216f74e473..86acd5c0cf 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 @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType import com.vitorpamplona.amethyst.commons.util.replace import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey 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 73ed9ff20e..07ff49216d 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 @@ -21,10 +21,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.commons.defaults.Constants +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent 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 aa9043aef2..5d17ed04d6 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 @@ -21,10 +21,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.commons.defaults.Constants +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingGeohashChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingGeohashChatSubAssembler.kt index 74060ea04f..48ef715cb4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingGeohashChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingGeohashChatSubAssembler.kt @@ -39,7 +39,7 @@ import java.util.concurrent.ConcurrentHashMap /** * Keeps the user's joined geohash location channels ([account.geohashList]) live - * in [com.vitorpamplona.amethyst.model.LocalCache] so they surface in the rooms + * in [com.vitorpamplona.amethyst.commons.model.cache.LocalCache] so they surface in the rooms * list and Home, mirroring [FollowingEphemeralChatSubAssembler]. Because the * events are ephemeral, this is a live tail — quiet cells simply have no last * message until one arrives. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt index 336b6d4877..14b24c43bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt @@ -36,10 +36,10 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.feeds.FeedContentState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.share_to_dm_start_new import com.vitorpamplona.amethyst.commons.resources.share_to_dm_title -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/AndroidChessAdapter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/AndroidChessAdapter.kt index 963cfd79a3..80b6a7ec27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/AndroidChessAdapter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/AndroidChessAdapter.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.nip64Chess.ChessConfig import com.vitorpamplona.amethyst.commons.nip64Chess.ChessEventBroadcaster import com.vitorpamplona.amethyst.commons.nip64Chess.ChessEventPublisher @@ -30,7 +31,6 @@ import com.vitorpamplona.amethyst.commons.nip64Chess.RelayFetchProgress import com.vitorpamplona.amethyst.commons.nip64Chess.RelayGameSummary import com.vitorpamplona.amethyst.commons.nip64Chess.subscription.ChessFilterBuilder import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip64Chess.ChessGameEnd import com.vitorpamplona.quartz.nip64Chess.ChessMoveEvent import com.vitorpamplona.quartz.nip64Chess.Color diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/dal/ChessFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/dal/ChessFeedFilter.kt index e51a5ac748..1afe8bcf7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/dal/ChessFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/dal/ChessFeedFilter.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip64Chess.game.ChessGameEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt index 4b45bfa7ac..cca44e07e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt index 2f9ec5a1b9..ea5403ca0b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityRulesLookup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityRulesLookup.kt index cd32a28dd0..5393ac47f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityRulesLookup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityRulesLookup.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.rules.CommunityRulesEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/dal/CommunitiesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/dal/CommunitiesFeedFilter.kt index fe372ee3ba..7469843e02 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/dal/CommunitiesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/dal/CommunitiesFeedFilter.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.dal import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.mapNotNullIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.DiscoverCommunityFeedFilter import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt index 499a91cd90..7da98867f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt @@ -71,6 +71,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.new_community_add_moderator import com.vitorpamplona.amethyst.commons.resources.new_community_add_moderator_placeholder @@ -87,7 +88,6 @@ import com.vitorpamplona.amethyst.commons.resources.new_community_relay_marker_r import com.vitorpamplona.amethyst.commons.resources.new_community_relays_hint import com.vitorpamplona.amethyst.commons.resources.new_community_rules import com.vitorpamplona.amethyst.commons.ui.components.Nip05OrPubkeyLine -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery 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 e4e5f8198e..4ac6cca8ed 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 @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index ed5ff94326..565620e593 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay @@ -46,7 +47,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor 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 d6e8a47c45..3bb50c255f 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 @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel 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 5dbd40284b..13de523110 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 @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent 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 51b6bff687..6317adc7ec 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 @@ -39,7 +39,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav 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 4ac0f53b5d..ddb61e1632 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 @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActiviti import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter @@ -31,7 +32,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community. import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.ui.dal.FilterByListParams 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 d5d387d007..203d586f19 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 @@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter @@ -50,7 +51,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.Aut import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled 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 480abdbe80..8e1c2fb557 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 @@ -44,6 +44,7 @@ import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter @@ -53,7 +54,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.Mute import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.community_about_topic -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout 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 1c3edc905b..d817affb56 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 @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.mapNotNullIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl 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 5beb3f634d..8b6364c1be 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 @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter @@ -32,7 +33,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community. import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.hasFreshDvmHeartbeat import com.vitorpamplona.amethyst.ui.dal.FilterByListParams 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 0ac05f6360..79d9ebed30 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 @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent 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 32a137507f..5366094a25 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 @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.ui.text.currentWord @@ -43,7 +44,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt index b7a69869b2..77f7e82ea3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmHeartbeatObservation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmHeartbeatObservation.kt index 97f28efee2..bcc740aa25 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmHeartbeatObservation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmHeartbeatObservation.kt @@ -40,9 +40,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.nip90DVMs.DvmHeartbeatRegistry import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.dvm_offline_banner -import com.vitorpamplona.amethyst.model.DvmHeartbeatRegistry import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap 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/dvms/DvmTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt index 78e65eed30..73cef0bddd 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,7 +28,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.MyAsyncImage 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 25ed12f438..286a7e321a 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 @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.observables.CreatedAtComparator import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/FavoriteAppManifestPreloader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/FavoriteAppManifestPreloader.kt index f5363d6489..b38eee524f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/FavoriteAppManifestPreloader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/FavoriteAppManifestPreloader.kt @@ -27,8 +27,8 @@ import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote 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/screen/loggedIn/emojipacks/browse/dal/BrowseEmojiSetsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/dal/BrowseEmojiSetsFeedFilter.kt index 668f4791d3..327d5e81d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/dal/BrowseEmojiSetsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/dal/BrowseEmojiSetsFeedFilter.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt index 920d1b43e2..352fc2da65 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt @@ -23,12 +23,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allUserFollows.AllUserFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt index 9457ddaac8..e721fbdb88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt @@ -24,11 +24,11 @@ import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.feeds.isRenderableRepost import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allUserFollows.AllUserFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt index 319dc4d92a..033edb6744 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache.checkGetOrCreateUser import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache.checkGetOrCreateUser import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent class FollowPackMembersFeedFilter( 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 91f53b621d..5e3caa267d 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 @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent 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 cd98baec03..3e9421f981 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 @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt index 205e095e36..d1cce88fec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt @@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.git_repo_external_host_body import com.vitorpamplona.amethyst.commons.resources.git_repo_external_host_title @@ -62,7 +63,6 @@ import com.vitorpamplona.amethyst.commons.resources.git_repo_stat_files import com.vitorpamplona.amethyst.commons.resources.git_repo_stat_tags import com.vitorpamplona.amethyst.commons.resources.git_repo_stat_updated import com.vitorpamplona.amethyst.commons.resources.git_untitled -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/dal/RepositoryIssuesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/dal/RepositoryIssuesFeedFilter.kt index ccd10c98fb..8748d7d376 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/dal/RepositoryIssuesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/dal/RepositoryIssuesFeedFilter.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.GitStatusIndex -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/dal/RepositoryPatchesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/dal/RepositoryPatchesFeedFilter.kt index 31a84ce7ae..dba219888c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/dal/RepositoryPatchesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/dal/RepositoryPatchesFeedFilter.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.GitStatusIndex -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt index 3df33d4b16..e9a34e4ad9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent 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 d296144a0e..99c78942fd 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 @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.feeds.isRenderableRepost import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent 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 9aceac442c..4164d69bf8 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 @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt index 3b37c4140a..4d69d08c88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt @@ -20,13 +20,13 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.filterMissingEvents import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.potentialRelaysToFindEvent import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/dal/HighlightsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/dal/HighlightsFeedFilter.kt index 5051f6d333..6fa403c631 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/dal/HighlightsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/dal/HighlightsFeedFilter.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.highlights.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent 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 cf79d6b90d..aa775d3a91 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 @@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPost @@ -54,7 +55,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.BooleanType -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory import com.vitorpamplona.amethyst.service.location.LocationState 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 abd1f6c5ae..146ceae389 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,10 +23,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.HomeFeedType import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent 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 8322b27b31..886dd9671a 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 @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveComplexFeedFilter import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter @@ -33,7 +34,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community. import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent 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 71065b32bd..3297759834 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,11 +24,11 @@ import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.feeds.isRenderableRepost import com.vitorpamplona.amethyst.commons.model.HomeFeedType import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt index 532a935675..85f932fee3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt @@ -29,9 +29,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt index efca237f8b..64a639001b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt @@ -31,8 +31,8 @@ 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.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel -import com.vitorpamplona.amethyst.model.LocalCache 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/lists/display/ShowUserSuggestions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt index bf4b45f20b..92810efc52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt @@ -63,10 +63,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.in_the_list import com.vitorpamplona.amethyst.commons.resources.search_and_add_a_user -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription import com.vitorpamplona.amethyst.ui.note.AboutDisplay import com.vitorpamplona.amethyst.ui.note.ClearTextIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt index 360ad99bf5..48b658bb0f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt @@ -70,6 +70,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.follow_set_broadcast import com.vitorpamplona.amethyst.commons.resources.follow_set_delete @@ -79,7 +80,6 @@ import com.vitorpamplona.amethyst.commons.resources.private_members_count import com.vitorpamplona.amethyst.commons.resources.public_members import com.vitorpamplona.amethyst.commons.resources.public_members_count import com.vitorpamplona.amethyst.commons.resources.search_and_add_a_user -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.ui.components.M3ActionRow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt index 205106e42d..e42905b659 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt @@ -63,12 +63,12 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.follow_pack_broadcast import com.vitorpamplona.amethyst.commons.resources.follow_pack_delete import com.vitorpamplona.amethyst.commons.resources.pack_actions_dialog_title import com.vitorpamplona.amethyst.commons.resources.search_and_add_a_user -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.ui.components.M3ActionRow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt index 76fa83e1ed..2c12c3a2b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt @@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.follow_set_copy_action_btn_label import com.vitorpamplona.amethyst.commons.resources.follow_set_copy_desc_label @@ -67,7 +68,6 @@ import com.vitorpamplona.amethyst.commons.resources.follow_set_rename_btn_label import com.vitorpamplona.amethyst.commons.resources.follow_set_rename_dialog_indicator_first_part import com.vitorpamplona.amethyst.commons.resources.follow_set_rename_dialog_indicator_second_part import com.vitorpamplona.amethyst.commons.resources.list_management_dialog_title -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.M3ActionDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt index 1cb30194d3..61030eb36b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt @@ -35,9 +35,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.follow_set_man_dialog_title2 -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/dal/LiveStreamsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/dal/LiveStreamsFeedFilter.kt index de76ab6728..999a069dd0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/dal/LiveStreamsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/dal/LiveStreamsFeedFilter.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter @@ -31,7 +32,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community. import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.ui.dal.FilterByListParams diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/dal/LongsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/dal/LongsFeedFilter.kt index b49dffd907..cfe0cbc3b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/dal/LongsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/dal/LongsFeedFilter.kt @@ -23,12 +23,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.commons.relayClient.video.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.SupportedContent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/AddToMusicPlaylistViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/AddToMusicPlaylistViewModel.kt index 23078cb368..20d9328279 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/AddToMusicPlaylistViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/AddToMusicPlaylistViewModel.kt @@ -24,9 +24,9 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt index bbbaaf04c2..986d66859e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.music import android.content.Context import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt index c7db6e6e21..31e5de4da7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt @@ -26,8 +26,8 @@ import android.net.Uri import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt index b0ad642776..42746dd5c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.music.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt index 958da2ee90..35bed93b10 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.music.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt index 961659c74b..9586878ed9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt @@ -62,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.napplet_connected_app_empty @@ -70,7 +71,6 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_policy_paranoid import com.vitorpamplona.amethyst.commons.resources.napplet_policy_reasonable import com.vitorpamplona.amethyst.favorites.rememberManifestIconModel import com.vitorpamplona.amethyst.favorites.rememberWebAppIconModel -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt index 4085b82674..9d7d9cd085 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt @@ -61,6 +61,7 @@ import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.live_stream_ended_tag import com.vitorpamplona.amethyst.commons.resources.nests_ended_ago @@ -69,7 +70,6 @@ import com.vitorpamplona.amethyst.commons.resources.nests_section_recently_ended import com.vitorpamplona.amethyst.commons.resources.nests_section_scheduled import com.vitorpamplona.amethyst.commons.resources.wallet_loading import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.SensitivityWarning diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/dal/NestsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/dal/NestsFeedFilter.kt index 550a029bb0..a43c0cc0e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/dal/NestsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/dal/NestsFeedFilter.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter @@ -32,7 +33,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community. import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index 4eb9691cfe..5176e76344 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.richtext.UrlParser @@ -44,7 +45,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lifecycle/NestRoomEventCollectors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lifecycle/NestRoomEventCollectors.kt index 82fd14e0b9..6e3fb4d01b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lifecycle/NestRoomEventCollectors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lifecycle/NestRoomEventCollectors.kt @@ -24,8 +24,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.experimental.nests.admin.AdminCommandEvent import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestJoinCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestJoinCard.kt index 91414afbb3..cf0c3f253e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestJoinCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestJoinCard.kt @@ -44,11 +44,11 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.nest_join import com.vitorpamplona.amethyst.commons.resources.nest_lobby_host_label -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt index c47b1b1b02..2c9c8e90d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt @@ -62,13 +62,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.nest_chat_empty import com.vitorpamplona.amethyst.commons.resources.nest_lobby_host_label import com.vitorpamplona.amethyst.commons.resources.nest_lobby_listeners_count import com.vitorpamplona.amethyst.commons.resources.nest_lobby_no_listeners import com.vitorpamplona.amethyst.commons.resources.nest_lobby_open_action -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe 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/nests/room/participants/ParticipantHostActionsSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt index c1e2c4f29a..1614f0e736 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt @@ -53,6 +53,7 @@ import androidx.core.net.toUri import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.nest_confirm_cancel import com.vitorpamplona.amethyst.commons.resources.nest_confirm_force_mute_body @@ -75,7 +76,6 @@ import com.vitorpamplona.amethyst.commons.resources.nest_toast_host_action_faile import com.vitorpamplona.amethyst.commons.resources.nest_toast_host_action_failed_title import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel import com.vitorpamplona.amethyst.commons.viewmodels.RoomSpeakerCatalog -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/HandRaiseQueueSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/HandRaiseQueueSection.kt index 9563f12939..1f362ccf87 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/HandRaiseQueueSection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/HandRaiseQueueSection.kt @@ -40,12 +40,12 @@ 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.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.nest_hand_raise_approve import com.vitorpamplona.amethyst.commons.resources.nest_hand_raise_queue_title import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel import com.vitorpamplona.amethyst.commons.viewmodels.RoomPresence -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/ParticipantsGrid.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/ParticipantsGrid.kt index 4c6357337d..8e0d0d96eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/ParticipantsGrid.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/ParticipantsGrid.kt @@ -457,7 +457,7 @@ private fun MemberCell( .let { if (member.absent) it.alpha(0.5f) else it } val user = remember(member.pubkey) { - com.vitorpamplona.amethyst.model.LocalCache + com.vitorpamplona.amethyst.commons.model.cache.LocalCache .getOrCreateUser(member.pubkey) } // Cache the long-click adapter per (pubkey, callback) tuple so a diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListSelectUserScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListSelectUserScreen.kt index 5ccda67c3b..94f3ca6077 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListSelectUserScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListSelectUserScreen.kt @@ -59,6 +59,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.import_follow_list import com.vitorpamplona.amethyst.commons.resources.import_follows_tips @@ -69,7 +70,6 @@ import com.vitorpamplona.amethyst.commons.resources.start_with_a_great_feed_by_f import com.vitorpamplona.amethyst.commons.resources.supports_npub_nip_05_hex_and_namecoin_bit_d_id import com.vitorpamplona.amethyst.commons.resources.tip import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route 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 7015ea6654..25f3230185 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 @@ -31,6 +31,8 @@ import com.vitorpamplona.amethyst.commons.feeds.LoadedFeedState import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.buzz.BuzzChannelInvite +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache.getNoteIfExists import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom import com.vitorpamplona.amethyst.commons.service.BundledInsert import com.vitorpamplona.amethyst.commons.service.BundledUpdate @@ -38,8 +40,6 @@ import com.vitorpamplona.amethyst.commons.ui.notifications.Card import com.vitorpamplona.amethyst.commons.ui.notifications.CardFeedState import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCache.getNoteIfExists import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.NotificationFeedOrderCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal.NotificationFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt index 057475cdf6..8b86a5c5da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt @@ -27,9 +27,9 @@ import com.patrykandpatrick.vico.compose.common.data.ExtraStore import com.patrykandpatrick.vico.compose.common.data.MutableExtraStore import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.BundledInsert import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.note.showCount 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 dccad9ac96..c9735bb746 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 @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.isMutedPublicChatMessage @@ -30,7 +31,6 @@ import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.buzz.jobs.JobErrorEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt index 050061aeb5..59873759a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt @@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.brought_to_you_by import com.vitorpamplona.amethyst.commons.resources.this_version_brought_to_you_by @@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.commons.resources.version_name import com.vitorpamplona.amethyst.commons.resources.zap_the_devs_description import com.vitorpamplona.amethyst.commons.resources.zap_the_devs_title import com.vitorpamplona.amethyst.commons.ui.components.appendLink -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.ReusableZapButton 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 index d573313ee9..d905efd48a 100644 --- 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 @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.ui.text.currentWord @@ -43,7 +44,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardComposePreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardComposePreview.kt index a1961d9c6b..c2b54665d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardComposePreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardComposePreview.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/dal/PictureFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/dal/PictureFeedFilter.kt index 291d23c13f..49aea905f5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/dal/PictureFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/dal/PictureFeedFilter.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip68Picture.PictureEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastAuthors.kt index 53f60bda2b..73efec26c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastAuthors.kt @@ -40,12 +40,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.podcast_author_verified import com.vitorpamplona.amethyst.commons.resources.podcast_role_cohost import com.vitorpamplona.amethyst.commons.resources.podcast_role_editor import com.vitorpamplona.amethyst.commons.resources.podcast_role_host -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent 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/podcasts/PodcastScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastScreen.kt index e0e5fd90bc..aa64532d26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastScreen.kt @@ -46,10 +46,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.podcast_no_episodes import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.feeds.FeedError import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt index 8543e72b5e..a508594c4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring import android.content.Context import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.podcasts.V4VSplitEditorState import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt index 1566ed11bb..46b0cdd517 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt @@ -24,9 +24,9 @@ import android.content.Context import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.podcasts.V4VSplitEditorState import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastAuthoringScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastAuthoringScreen.kt index d1ddf722fc..05ba2110b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastAuthoringScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastAuthoringScreen.kt @@ -59,6 +59,7 @@ import coil3.compose.rememberAsyncImagePainter import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.podcast_create_show_hint import com.vitorpamplona.amethyst.commons.resources.podcast_create_your_show @@ -66,7 +67,6 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_no_episodes_yet import com.vitorpamplona.amethyst.commons.resources.podcast_tap_to_edit_show import com.vitorpamplona.amethyst.commons.resources.podcast_untitled import com.vitorpamplona.amethyst.commons.resources.podcast_your_podcast -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/OnePodcastEpisodesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/OnePodcastEpisodesFeedFilter.kt index fc1b5f2624..5275645f6e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/OnePodcastEpisodesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/OnePodcastEpisodesFeedFilter.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nipXXPodcasting20.episode.Podcasting20EpisodeEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/OnePodcastFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/OnePodcastFeedViewModel.kt index f1ce9ebbac..824cc69c3b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/OnePodcastFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/OnePodcastFeedViewModel.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastEpisodesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastEpisodesFeedFilter.kt index 3022ab13a7..b98f416758 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastEpisodesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastEpisodesFeedFilter.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nipXXPodcasting20.episode.Podcasting20EpisodeEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastsFeedFilter.kt index 54bff2d535..3026c56aa7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastsFeedFilter.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/ClosedPollsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/ClosedPollsFeedFilter.kt index 073f608c40..f076b195d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/ClosedPollsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/ClosedPollsFeedFilter.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/OpenPollsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/OpenPollsFeedFilter.kt index 6f2371177e..9869ae301f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/OpenPollsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/OpenPollsFeedFilter.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/PollsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/PollsFeedFilter.kt index ae785a66e1..43294a5591 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/PollsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/PollsFeedFilter.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/dal/ProductsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/dal/ProductsFeedFilter.kt index 21e4f67820..bb061fb74b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/dal/ProductsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/dal/ProductsFeedFilter.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.products.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent 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 5a55cdf4ef..667876fa7f 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 @@ -65,6 +65,7 @@ import androidx.compose.ui.unit.IntSize import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.profile.UserProfileFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.gallery @@ -72,7 +73,6 @@ import com.vitorpamplona.amethyst.commons.resources.mutual import com.vitorpamplona.amethyst.commons.resources.notes import com.vitorpamplona.amethyst.commons.resources.profile_tab_apps import com.vitorpamplona.amethyst.commons.resources.replies -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser import com.vitorpamplona.amethyst.ui.feeds.UserBlockedFeed import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/apps/dal/UserProfileAppsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/apps/dal/UserProfileAppsFeedFilter.kt index 106c550663..0c285398e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/apps/dal/UserProfileAppsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/apps/dal/UserProfileAppsFeedFilter.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.apps.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent 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 36fbe239b5..ddc089dfe1 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 @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark 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 70541e8c39..9d0d1be771 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 @@ -25,8 +25,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt index ef4afe3172..038c9c118a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt @@ -25,8 +25,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted 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 0425c0d86a..676329fac8 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 @@ -24,9 +24,9 @@ import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayPaymentTargets.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayPaymentTargets.kt index 51739bcf4b..102e3fb2ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayPaymentTargets.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayPaymentTargets.kt @@ -29,8 +29,8 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.payments.PaymentTargetTypes -import com.vitorpamplona.amethyst.model.LocalCache 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/profile/header/ProfilePaymentRailChips.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt index 43e0949b2c..1bd1ea1185 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt @@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip01Core.UserInfo import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.bolt12_lightning_offer @@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.commons.resources.clink_lightning_offer import com.vitorpamplona.amethyst.commons.resources.send_payment_method_cashu import com.vitorpamplona.amethyst.commons.resources.send_payment_method_lightning import com.vitorpamplona.amethyst.commons.resources.send_payment_method_onchain -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.util.setText diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt index 62a2c55d75..2f9e87e4c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent import com.vitorpamplona.quartz.utils.flattenToSet 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 6d42b5f4c9..c3d90b26a6 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 @@ -63,12 +63,12 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.badge_award_image import com.vitorpamplona.amethyst.commons.resources.badge_award_image_for import com.vitorpamplona.amethyst.commons.resources.badge_untitled import com.vitorpamplona.amethyst.commons.resources.profile_badges_header -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/identity/UserExternalIdentitiesViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/identity/UserExternalIdentitiesViewModel.kt index 3ea1e213dd..9a5c175da6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/identity/UserExternalIdentitiesViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/identity/UserExternalIdentitiesViewModel.kt @@ -25,7 +25,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt index 28db360db3..4a1654ac55 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt @@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.commons.feeds.isRenderableRepost import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent 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 1556915f2e..f649abb356 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 @@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.commons.feeds.isRenderableRepost import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt index 4a76701443..abe067e213 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.payments.PaymentSource import com.vitorpamplona.amethyst.commons.model.payments.PaymentSourceResolver import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult @@ -83,7 +84,6 @@ import com.vitorpamplona.amethyst.commons.resources.zap_type_private_explainer import com.vitorpamplona.amethyst.commons.resources.zap_type_public import com.vitorpamplona.amethyst.commons.resources.zap_type_public_explainer import com.vitorpamplona.amethyst.model.DEFAULT_ONCHAIN_ZAP_SATS -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.MIN_ONCHAIN_ZAP_SATS import com.vitorpamplona.amethyst.service.ClinkOfferPayer import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/pinnedNotes/dal/UserProfilePinnedNotesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/pinnedNotes/dal/UserProfilePinnedNotesFeedFilter.kt index de4838deca..a6581aaa69 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/pinnedNotes/dal/UserProfilePinnedNotesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/pinnedNotes/dal/UserProfilePinnedNotesFeedFilter.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.dal import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip51Lists.PinListEvent class UserProfilePinnedNotesFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt index 6e4ae58348..e4ff0bd130 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem 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/profile/zaps/dal/UserProfileZapsViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt index 846b4346d8..4e718ef67e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt @@ -26,8 +26,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -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.nip57Zaps.LnZapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt index 2c3e605062..d30cb73aa2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/dal/RelayFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/dal/RelayFeedFilter.kt index 3f68ab78d4..13e15c7c88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/dal/RelayFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/dal/RelayFeedFilter.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/dal/SingleRelayFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/dal/SingleRelayFeedViewModel.kt index ca6549d7ad..9403ea9f3e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/dal/SingleRelayFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/dal/SingleRelayFeedViewModel.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl 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 bc79b4bd89..b34fdbeceb 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 @@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.accepts_up_to import com.vitorpamplona.amethyst.commons.resources.access_control @@ -153,7 +154,6 @@ import com.vitorpamplona.amethyst.commons.resources.version import com.vitorpamplona.amethyst.commons.resources.yes import com.vitorpamplona.amethyst.commons.ui.components.appendLink import com.vitorpamplona.amethyst.commons.util.timeDiffAgoShortish -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelaySuggestionState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelaySuggestionState.kt index 220d558aae..21df27772c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelaySuggestionState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelaySuggestionState.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow 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 4f6de20024..8e7d24af54 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 @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt index 85a25a157a..1a58a2cff7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt @@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State import com.vitorpamplona.amethyst.commons.relayManagement.Nip86Retriever import com.vitorpamplona.amethyst.commons.resources.Res @@ -118,7 +119,6 @@ import com.vitorpamplona.amethyst.commons.resources.relay_management_relay_name import com.vitorpamplona.amethyst.commons.resources.relay_management_remove import com.vitorpamplona.amethyst.commons.resources.relay_management_title import com.vitorpamplona.amethyst.commons.resources.search_and_add_a_user -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementViewModel.kt index 764d71794d..9b81f386fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementViewModel.kt @@ -24,9 +24,9 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayManagement.Nip86Retriever import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip86RelayManagement.Nip86Client import com.vitorpamplona.quartz.nip86RelayManagement.rpc.AllowedPubkey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt index f0315bbf43..c55c3122d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt @@ -60,6 +60,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.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilter @@ -81,7 +82,6 @@ import com.vitorpamplona.amethyst.commons.resources.active_subs_scope_global import com.vitorpamplona.amethyst.commons.resources.active_subs_scope_muted import com.vitorpamplona.amethyst.commons.resources.active_subs_unattributed import com.vitorpamplona.amethyst.commons.resources.marmot_group_fallback_name -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo 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/search/SearchBarViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt index ebaa14bc7b..c2ce2b0460 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 @@ -33,6 +33,7 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.actions.ConcordActions import com.vitorpamplona.amethyst.commons.feeds.InvalidatableContent import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.search.SearchQueryState import com.vitorpamplona.amethyst.commons.search.RenderableKinds import com.vitorpamplona.amethyst.commons.search.SearchHistory @@ -45,7 +46,6 @@ import com.vitorpamplona.amethyst.commons.search.SearchSource import com.vitorpamplona.amethyst.commons.search.SearchState import com.vitorpamplona.amethyst.commons.search.wholeInputNip19 import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.userUriPrefixes import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder 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 1b13d64330..4a6fde7ba4 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 @@ -84,6 +84,7 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.npub_hex_username import com.vitorpamplona.amethyst.commons.resources.search_by_hashtag @@ -118,7 +119,6 @@ import com.vitorpamplona.amethyst.commons.ui.search.GroupCandidate import com.vitorpamplona.amethyst.commons.ui.search.SEARCH_PICKER_LIMIT import com.vitorpamplona.amethyst.commons.ui.search.SearchFieldState import com.vitorpamplona.amethyst.commons.ui.search.TokenizedSearchField -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.model.preferences.DataStoreSearchHistoryStorage import com.vitorpamplona.amethyst.service.location.CachedReversedGeoLocations diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ResourceUsageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ResourceUsageScreen.kt index 3d37ab7032..d6557f22ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ResourceUsageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ResourceUsageScreen.kt @@ -62,6 +62,7 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.MemorySnapshot import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.collectMemorySnapshot +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.resource_usage_alwayson_explanation import com.vitorpamplona.amethyst.commons.resources.resource_usage_alwayson_settings_button @@ -73,7 +74,6 @@ import com.vitorpamplona.amethyst.commons.resources.resource_usage_send_explanat import com.vitorpamplona.amethyst.commons.resources.resource_usage_tor_explanation import com.vitorpamplona.amethyst.commons.resources.resource_usage_tor_settings_button import com.vitorpamplona.amethyst.commons.service.crashreports.DEV_REPORT_PUBKEY -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.resourceusage.ResourceUsageAccountant import com.vitorpamplona.amethyst.service.resourceusage.ResourceUsageReportAssembler import com.vitorpamplona.amethyst.service.resourceusage.ResourceUsageReportAssembler.Companion.formatBytes 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 ecc970c800..a4e4693d5e 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 @@ -27,8 +27,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.feeds.InvalidatableContent +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.BundledUpdate -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.utils.Log 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 62dac1eee8..782e3a60f3 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 @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/MutedThreadsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/MutedThreadsFeedFilter.kt index 0ebd6f32ed..bc2ff4f20e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/MutedThreadsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/MutedThreadsFeedFilter.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache class MutedThreadsFeedFilter( val account: Account, 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 4299a90a11..579b043143 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 @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache class SpammerAccountsFeedFilter( val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/dal/ShortsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/dal/ShortsFeedFilter.kt index 5f72c74f6a..4e5390209a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/dal/ShortsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/dal/ShortsFeedFilter.kt @@ -23,12 +23,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.commons.relayClient.video.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.SupportedContent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/dal/SoftwareAppsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/dal/SoftwareAppsFeedFilter.kt index a08289a857..0f3bb8cdf1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/dal/SoftwareAppsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/dal/SoftwareAppsFeedFilter.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent 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 170f0d1829..591b5c5eee 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 @@ -78,6 +78,7 @@ import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.hi_there_is_this_still_available import com.vitorpamplona.amethyst.commons.resources.send_a_direct_message @@ -91,7 +92,6 @@ import com.vitorpamplona.amethyst.commons.ui.note.RenderFedimint import com.vitorpamplona.amethyst.commons.ui.note.RenderMintRecommendation import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel import com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.ui.components.LoadNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/AndroidLevelFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/AndroidLevelFeedViewModel.kt index 370cb640cd..055407a1ed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/AndroidLevelFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/AndroidLevelFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel -import com.vitorpamplona.amethyst.model.LocalCache /** * Android-specific LevelFeedViewModel base class that provides LocalCache as the cache provider. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt index 2b3dc66320..46c2d4ff4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.viewmodels.thread.ThreadFeedFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache class ThreadFeedViewModel( account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/dal/UrlFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/dal/UrlFeedFilter.kt index f4b345b5b5..e5895e1a8a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/dal/UrlFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/dal/UrlFeedFilter.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.url.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip22Comments.CommentEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/dal/UrlFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/dal/UrlFeedViewModel.kt index 8f81ba57d7..3719e8f6bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/dal/UrlFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/dal/UrlFeedViewModel.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.url.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl 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 0db05fecc3..b9b67e2ffc 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 @@ -23,12 +23,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.commons.relayClient.video.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuMintRecommendationsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuMintRecommendationsScreen.kt index 5977a94a18..837b4ca3a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuMintRecommendationsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuMintRecommendationsScreen.kt @@ -66,13 +66,13 @@ import com.vitorpamplona.amethyst.commons.hashtags.Cashu import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cashu_settings_add_recommendation import com.vitorpamplona.amethyst.commons.resources.cashu_settings_delete_confirm_title import com.vitorpamplona.amethyst.commons.resources.cashu_settings_delete_recommendation import com.vitorpamplona.amethyst.commons.resources.cashu_settings_my_recommendations import com.vitorpamplona.amethyst.commons.resources.cashu_settings_no_recommendations -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe 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/wallet/CashuMintsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuMintsScreen.kt index 2925550791..790d396f73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuMintsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuMintsScreen.kt @@ -67,6 +67,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cashu_add_mint import com.vitorpamplona.amethyst.commons.resources.cashu_mint_reachable @@ -78,7 +79,6 @@ import com.vitorpamplona.amethyst.commons.resources.cashu_remove_mint import com.vitorpamplona.amethyst.commons.resources.cashu_verify import com.vitorpamplona.amethyst.commons.resources.cashu_wallet_autosaves import com.vitorpamplona.amethyst.commons.resources.cashu_wallet_saving -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip60Cashu.CashuMintDirectoryEntry import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt index de6bf9d762..433a37f2f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt @@ -85,6 +85,7 @@ import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cashu_action_receive import com.vitorpamplona.amethyst.commons.resources.cashu_action_redeem @@ -144,7 +145,6 @@ import com.vitorpamplona.amethyst.commons.resources.cashu_wallet_title import com.vitorpamplona.amethyst.commons.resources.paste_from_clipboard import com.vitorpamplona.amethyst.commons.resources.topup_mint_action import com.vitorpamplona.amethyst.commons.resources.wallet_sats -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip60Cashu.CashuWalletHistoryEoseManager import com.vitorpamplona.amethyst.ui.components.util.getText import com.vitorpamplona.amethyst.ui.components.util.setText diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainTransactionsViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainTransactionsViewModel.kt index 190ed1b690..49f7a1e4cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainTransactionsViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainTransactionsViewModel.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nipBCOnchainZaps.chain.BitcoinAddressTx diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt index a718d97cc7..881d8311e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt @@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.onchain.DustRecipientException import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendError import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult @@ -105,7 +106,6 @@ import com.vitorpamplona.amethyst.commons.resources.onchain_send_success import com.vitorpamplona.amethyst.commons.resources.onchain_send_title import com.vitorpamplona.amethyst.commons.resources.onchain_send_to import com.vitorpamplona.amethyst.model.DEFAULT_ONCHAIN_ZAP_SATS -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.MIN_ONCHAIN_ZAP_SATS import com.vitorpamplona.amethyst.ui.components.namecoin.NamecoinResolutionRow import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletDiscovery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletDiscovery.kt index 40ce68ec00..41bd6b695a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletDiscovery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletDiscovery.kt @@ -60,7 +60,7 @@ import kotlin.coroutines.cancellation.CancellationException * [Semaphore] window inside a [supervisorScope]; * - paginates each relay individually through Quartz's [fetchAllPages]; * - runs on a **fresh** [INostrClient] (built by [clientBuilder]) so the crawled - * events never pollute the production [com.vitorpamplona.amethyst.model.LocalCache]. + * events never pollute the production [com.vitorpamplona.amethyst.commons.model.cache.LocalCache]. * * Unlike EventSync this neither republishes nor mutates state — it only collects * and hands the distinct events back to the wizard for parsing/verification. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/dal/WebBookmarkFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/dal/WebBookmarkFeedFilter.kt index e3dbc9ca80..24af3b3231 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/dal/WebBookmarkFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/dal/WebBookmarkFeedFilter.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/dal/WorkoutFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/dal/WorkoutFeedFilter.kt index b5c98dff33..48397e0c3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/dal/WorkoutFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/dal/WorkoutFeedFilter.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.fitness.workout.WorkoutRecordEvent diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt index 3d484262cc..f126658623 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Dao import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.model.LocalCache.getOrCreateAddressableNoteInternal +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache.getOrCreateAddressableNoteInternal import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip19Bech32.entities.NNote diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarReminderCandidatesTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarReminderCandidatesTest.kt index 74347b6d61..99f3195d38 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarReminderCandidatesTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarReminderCandidatesTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.calendar -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.service.calendar.CalendarReminderWorker import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/AppSpecificDataRepublishTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/AppSpecificDataRepublishTest.kt index 95854ab122..94d1225985 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/AppSpecificDataRepublishTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/AppSpecificDataRepublishTest.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.model import android.os.Looper +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.core.nextCreatedAtToSupersede import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/BuzzDmNotificationResolutionTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/BuzzDmNotificationResolutionTest.kt index 5e588e814d..1ede333c8f 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/BuzzDmNotificationResolutionTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/BuzzDmNotificationResolutionTest.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model import android.os.Looper import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event import com.vitorpamplona.quartz.buzz.workspace.buzzParticipants import com.vitorpamplona.quartz.buzz.workspace.isBuzzDm diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/BuzzWorkspaceChannelTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/BuzzWorkspaceChannelTest.kt index 1621905d52..30766d0f2c 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/BuzzWorkspaceChannelTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/BuzzWorkspaceChannelTest.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model import android.os.Looper import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect import com.vitorpamplona.amethyst.commons.model.buzz.BuzzWorkspaceStates +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.latestBuzzEdit import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.quartz.buzz.stream.StreamMessageEditEvent diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/DvmHeartbeatTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/DvmHeartbeatTest.kt index 87e693af34..22fbbb4e67 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/DvmHeartbeatTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/DvmHeartbeatTest.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.nip90DVMs.DvmHeartbeatRegistry import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip90Dvms.dvmHeartbeat.DvmHeartbeatEvent diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/EntityRatingIngestionTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/EntityRatingIngestionTest.kt index 012478cb99..609857aaf5 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/EntityRatingIngestionTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/EntityRatingIngestionTest.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.model import com.vitorpamplona.amethyst.commons.model.HomeFeedType +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.experimental.publications.PublicationIndexEvent import com.vitorpamplona.quartz.experimental.ratings.EntityRatingEvent import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/LocalCacheSearchParityTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/LocalCacheSearchParityTest.kt index 865f6c1457..6bf410abc4 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/LocalCacheSearchParityTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/LocalCacheSearchParityTest.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import kotlinx.serialization.json.Json diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/ReportNamingIndexIngestionTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/ReportNamingIndexIngestionTest.kt index 8eabafc9e1..5d64a594db 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/ReportNamingIndexIngestionTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/ReportNamingIndexIngestionTest.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType import org.junit.Assert.assertEquals diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListAddRelayTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListAddRelayTest.kt index 43838456fc..77851b0e7d 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListAddRelayTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListAddRelayTest.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.model.nip51Lists import android.os.Looper +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteAlgoFeeds/FavoriteAlgoFeedTopNavFilterTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteAlgoFeeds/FavoriteAlgoFeedTopNavFilterTest.kt index 0e8918b99f..a8872f4ad3 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteAlgoFeeds/FavoriteAlgoFeedTopNavFilterTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteAlgoFeeds/FavoriteAlgoFeedTopNavFilterTest.kt @@ -108,7 +108,7 @@ class FavoriteAlgoFeedTopNavFilterTest { // passing a LocalCache is only needed because the method demands it; // FavoriteAlgoFeedTopNavFilter.startValue doesn't actually consult it. - val set = filter.startValue(com.vitorpamplona.amethyst.model.LocalCache) + val set = filter.startValue(com.vitorpamplona.amethyst.commons.model.cache.LocalCache) assertTrue(set.requestIds.isEmpty()) } @@ -124,7 +124,7 @@ class FavoriteAlgoFeedTopNavFilterTest { requestId = "9".repeat(64), ) - val set = filter.startValue(com.vitorpamplona.amethyst.model.LocalCache) + val set = filter.startValue(com.vitorpamplona.amethyst.commons.model.cache.LocalCache) assertTrue(set.requestIds == setOf("9".repeat(64))) } } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/notifications/NotificationContentMentionTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/notifications/NotificationContentMentionTest.kt index 7b2783a490..ba5ce493c0 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/notifications/NotificationContentMentionTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/notifications/NotificationContentMentionTest.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.notifications import android.os.Looper -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/AddressableAuthorRelayLoaderSubAssemblerTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/AddressableAuthorRelayLoaderSubAssemblerTest.kt index 7ac2c9c86d..1e30d86b88 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/AddressableAuthorRelayLoaderSubAssemblerTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/AddressableAuthorRelayLoaderSubAssemblerTest.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryState import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.AddressableAuthorRelayLoaderSubAssembler import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Address import io.mockk.every import io.mockk.mockk diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index efbf6a8f94..4d8512e845 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -776,10 +776,9 @@ Options, for the maintainer to pick: `NappletIdentityWatch`); `ScheduledPostStore` Jackson+`java.io.File` → kotlinx-serialization+okio (frees `ScheduledPostWorkGate`). `LargeSoftCache` stays parked (needs a WeakReference expect/actual). -- **Wave 2: LocalCache move-group** (~200 files behind it) — **blocked on a - design decision**: `CachePruner`/`CacheSearch` call - `Account.isFollowing(...)`; either `IAccount` grows it (DesktopIAccount - must implement) or they take a narrower interface (recommended). +- **Wave 2: LocalCache move-group** — **part A landed (2026-09-19)**, see the + section below. Part B (repoint `desktopApp`, delete `DesktopLocalCache.kt`) + is the remaining half. - **Wave 3: strings bridge** — **BUILT (2026-09-01)**. The pieces: - `commons/ui/StringRes.kt` (commonMain): `stringRes(StringResource)`, formatted + plural variants, `loadStringRes`/`loadPluralStringRes` for @@ -826,3 +825,98 @@ Options, for the maintainer to pick: gradle re-extract. - stdlib atomics have no `incrementAndFetch()` here — use `addAndFetch(1)`; `withLock {}` can't assign outer `val`s — restructure to lambda-return. + + +### Wave 2 part A landed — `LocalCache` is in `commons` (2026-09-19) + +`LocalCache` (4,021 lines) and its move-group now live in +`commons/src/jvmAndroid/…/commons/model/`. `desktopApp` is untouched and still +runs `DesktopLocalCache`; repointing it and deleting that 1,177-line fork is +part B. + +**Where things went** + +| From `amethyst/…/model/` | To `commons/…/commons/model/` | +|---|---| +| `LocalCache.kt` | `cache/LocalCache.kt` | +| `AntiSpamFilter.kt` | `cache/AntiSpamFilter.kt` | +| `CachePruner.kt` | `cache/CachePruner.kt` | +| `CacheSearch.kt` | `cache/CacheSearch.kt` | +| `DvmHeartbeatRegistry.kt` | `nip90DVMs/DvmHeartbeatRegistry.kt` | +| `nipBCOnchainZaps/OnchainZapResolver.kt` | `nipBCOnchainZaps/OnchainZapResolver.kt` | + +All in `jvmAndroid`: `LocalCache` spills NIP-95 blobs through `java.io.File`, +and the other five either name `LocalCache` or use `java.util.concurrent`. + +**The seam: `LocalCacheHost`** + +A new port in `commons/…/model/cache/LocalCacheHost.kt` collects everything the +cache needed from `Amethyst.instance` and friends — `scope`, `isDebug`, +`nip95BlobDir`, `relayStats`, `relaySelfPubKey(relay)`, `assertNotMainThread()`. +Every member has a default that is the honest answer for a host that supplies +none of them, so the cache runs shell-less (Desktop, tests). `LocalCache.appHost` +is the install point; Android installs `AmethystLocalCacheHost` from `AppModules` +next to `val cache`, before any event is consumed. Named `appHost`, not `host`, +because `host` is already a domain word in that file (NIP-29 host relay, +gift-wrap host). + +The whole seam cut is 101 changed lines in a 4,021-line file: + +| Was | Now | +|---|---| +| `Amethyst.instance.nip11Cache.getFromCache(relay).self` | `appHost.relaySelfPubKey(relay)` | +| `Amethyst.instance.applicationIOScope.launch` (×2) | `appHost.scope.launch` | +| `Amethyst.instance.nip95cache` | `appHost.nip95BlobDir` | +| `isDebug` (×2) | `appHost.isDebug` | +| `checkNotInMainThread()` | `appHost.assertNotMainThread()` | +| `ui.note.dateFormatter` | `commons.util.dateFormatter` (the twin already existed) | + +`nip95BlobDir` is nullable, so the `FileStorageEvent` path had to grow one +branch: with nowhere to spill to, the note keeps the event whole instead of +loading a content-stripped copy whose bytes are on no disk. Android always +supplies the directory, so its behaviour is unchanged. + +**The design question the plan flagged, resolved** + +`CachePruner`/`CacheSearch` took `Account`. It turned out `IAccount` did not +need to grow anything and Desktop needed no change: + +- `CacheSearch.findUsersStartingWith(…, forAccount: IAccount?)` — `isHidden` + and `hiddenWordsCase` are already on `IAccount`, and `isFollowing(user)` is + by definition `user.pubkeyHex in followingKeySet()`, which is too. Hoisting + the set out of the per-candidate lambda also stops rebuilding it per result. +- `CachePruner.pruneHiddenMessages(account: IAccount)` — `Channel.pruneHiddenMessages` + already took `IAccount`. +- `CachePruner.pruneHiddenEvents(hidden: LiveHiddenUsers)` — takes the mute list + by value, the idiom `ICacheProvider.findNotesMatching` already documents. + `IAccount` exposes hidden users only as hash codes, which would prune authors + the reader never muted. + +**Other pieces** + +- `ILocalCache` was declared inline in `LocalCache.kt` and used nowhere else; + hoisted to `commons/…/model/cache/ILocalCache.kt` (commonMain) as the + write-side port next to the read-side `ICacheProvider`. +- `AntiSpamFilter`: `android.util.LruCache` → `androidx.collection.LruCache`, + already the commons idiom. Its `get` is Kotlin-nullable where the platform + class returned a platform type, so the two duplicate checks now read each + entry once into a local and smart-cast. It takes its host as `() -> LocalCacheHost` + rather than a value, because the cache that owns it is built before the shell + installs one. +- `njumpLink` moved from `ui/note/NoteQuickActionMenu.kt` to + `commons/…/util/ExternalLinks.kt` (AntiSpamFilter logs one); the app's four + call sites import it from there. +- `OnchainZapResolver.onchainTipHeightFlow` dropped its + `runCatching { Amethyst.instance }` fallback — `cache.appHost.scope` always + answers. + +**Left in `amethyst` on purpose:** `MiniFhir.kt`, which the original move-group +listed. It is clean and movable, but `LocalCache` never referenced it (the hit +was the phrase "Resource-usage ledger" in a comment), so it unlocks nothing here +and would only widen the diff. `OnchainWalletState` likewise stays — `Account` +and `RailCapability` use it, `OnchainZapResolver` does not. + +**Verified:** `:commons:compileKotlinJvm`, `:amethyst:compileFdroidDebugKotlin`, +`:desktopApp:compileKotlin`, `:cli:compileKotlin`, `:commons:verifyKmpPurity`, +`:commons:jvmTest`, `:cli:test`, `:amethyst:testPlayDebugUnitTest`, +`spotlessApply`. diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ILocalCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ILocalCache.kt new file mode 100644 index 0000000000..615148ad29 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ILocalCache.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.commons.model.cache + +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +/** + * The write-side port of the event cache: what a relay connection needs to call when a relay + * confirms it holds an event, without naming the concrete cache. + * + * Kept separate from [ICacheProvider], which is the read side the model layer uses. + */ +interface ILocalCache { + /** + * Records that [relay] is known to hold [eventId] — because it served it, or acknowledged + * it with an `OK`. + * + * Default no-op so a cache that keeps no per-relay provenance need not pretend to. + */ + fun markAsSeen( + eventId: String, + relay: NormalizedRelayUrl, + ) { + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/ExternalLinks.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/ExternalLinks.kt new file mode 100644 index 0000000000..5166685d9c --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/ExternalLinks.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.commons.util + +/** + * An `https://njump.to/…` link for a NIP-19 bech32 entity, for handing a nostr event to + * something that does not speak nostr — a share sheet, a browser, or a log line meant to be + * opened by a human. + */ +fun njumpLink(nip19BechAddress: String) = "https://njump.to/$nip19BechAddress" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/AntiSpamFilter.kt similarity index 84% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/AntiSpamFilter.kt index 752492a5c3..d4a91650f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/AntiSpamFilter.kt @@ -18,11 +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.amethyst.model +package com.vitorpamplona.amethyst.commons.model.cache -import android.util.LruCache -import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.ui.note.njumpLink +import androidx.collection.LruCache +import com.vitorpamplona.amethyst.commons.util.njumpLink import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event @@ -42,7 +41,13 @@ data class Spammer( fun shouldHide() = duplicatedEventIds.size >= 5 || duplicatedEventAddresses.size >= 5 } -class AntiSpamFilter { +class AntiSpamFilter( + /** + * Where duplicate reports go. Read on each hit rather than captured, because the cache that + * owns this filter is built long before the app shell installs its [LocalCacheHost]. + */ + private val host: () -> LocalCacheHost = { LocalCacheHost }, +) { val recentEventIds = LruCache(2000) val recentAddressables = LruCache(2000) val spamMessages = LruCache(1000) @@ -78,14 +83,14 @@ class AntiSpamFilter { val address = event.address() // normal event + // existingAddress may be null if the first duplicate was evicted from the LRU cache + // while the spammer record still matches this hash. + val existingAddress = recentAddressables[hash] + val knownSpammer = spamMessages[hash] if ( - (recentAddressables[hash] != null && recentAddressables[hash] != address) || - (spamMessages[hash] != null && !spamMessages[hash].duplicatedEventAddresses.contains(address)) + (existingAddress != null && existingAddress != address) || + (knownSpammer != null && !knownSpammer.duplicatedEventAddresses.contains(address)) ) { - // may be null if the first duplicate was evicted from the LRU cache - // while the spammer record still matches this hash. - val existingAddress = recentAddressables[hash] - val link2 = njumpLink(NAddress.create(event.kind, event.pubKey, event.dTag(), relay)) val link1 = existingAddress?.let { njumpLink(NAddress.create(it.kind, it.pubKeyHex, it.dTag, relay)) } ?: link2 @@ -101,9 +106,10 @@ class AntiSpamFilter { val spammer = logOffender(hash, event) if (spammer.shouldHide() && relay != null) { - Amethyst.instance.relayStats - .get(relay) - .newSpam(link1, link2) + host() + .relayStats + ?.get(relay) + ?.newSpam(link1, link2) } flowSpam.tryEmit(AntiSpamState(this)) @@ -115,9 +121,10 @@ class AntiSpamFilter { } else { // normal event val existingEvent = recentEventIds[hash] + val knownSpammer = spamMessages[hash] if ( (existingEvent != null && existingEvent != event.id) || - (spamMessages[hash] != null && !spamMessages[hash].duplicatedEventIds.contains(event.id)) + (knownSpammer != null && !knownSpammer.duplicatedEventIds.contains(event.id)) ) { val link2 = njumpLink(NEvent.create(event.id, null, null, relay)) // existingEvent may be null if the first duplicate was evicted from the @@ -136,9 +143,10 @@ class AntiSpamFilter { val spammer = logOffender(hash, event) if (spammer.shouldHide() && relay != null) { - Amethyst.instance.relayStats - .get(relay) - .newSpam(link1, link2) + host() + .relayStats + ?.get(relay) + ?.newSpam(link1, link2) } flowSpam.tryEmit(AntiSpamState(this)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/CachePruner.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/CachePruner.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt index 40679c575e..bf37e2c96a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/CachePruner.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt @@ -18,14 +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.model +package com.vitorpamplona.amethyst.commons.model.cache import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.IAccount +import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel -import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.buzz.stream.StreamMessageEditEvent import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChatEditEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent @@ -85,7 +85,7 @@ class CachePruner( private fun pruneHiddenMessagesChannel( channel: Channel, - account: Account, + account: IAccount, ) { val toBeRemoved = channel.pruneHiddenMessages(account) @@ -106,7 +106,7 @@ class CachePruner( } } - fun pruneHiddenMessages(account: Account) { + fun pruneHiddenMessages(account: IAccount) { cache.ephemeralChannels.forEach { _, channel -> pruneHiddenMessagesChannel(channel, account) } @@ -162,7 +162,7 @@ class CachePruner( } fun pruneOldMessages() { - checkNotInMainThread() + cache.appHost.assertNotMainThread() cache.ephemeralChannels.forEach { _, channel -> pruneOldMessagesChannel(channel) @@ -310,7 +310,7 @@ class CachePruner( } fun pruneRepliesAndReactions(accounts: Set) { - checkNotInMainThread() + cache.appHost.assertNotMainThread() val toBeRemoved = cache.notes.filter { _, note -> @@ -448,7 +448,7 @@ class CachePruner( } fun pruneExpiredEvents() { - checkNotInMainThread() + cache.appHost.assertNotMainThread() val now = TimeUtils.now() val versionsToBeRemoved = cache.notes.filter { _, it -> it.event?.isExpirationBefore(now) == true } @@ -473,13 +473,20 @@ class CachePruner( } } - fun pruneHiddenEvents(account: Account) { - checkNotInMainThread() + /** + * Drops every event authored by a muted or blocked user. + * + * Takes the mute list by value rather than the account that holds it: the pubkeys are all + * this needs, and [IAccount] exposes hidden users only as hash codes, which would match + * authors the reader never muted. + */ + fun pruneHiddenEvents(hidden: LiveHiddenUsers) { + cache.appHost.assertNotMainThread() val childrenToBeRemoved = mutableListOf() val toBeRemoved = - account.hiddenUsers.flow.value.hiddenUsers.flatMap { userHex -> + hidden.hiddenUsers.flatMap { userHex -> (cache.notes.filter { _, it -> it.event?.pubKey == userHex } + cache.addressables.filter { _, it -> it.event?.pubKey == userHex }).toSet() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/CacheSearch.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/CacheSearch.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt index 0c6a81c41d..7c9d692797 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/CacheSearch.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt @@ -18,17 +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 +package com.vitorpamplona.amethyst.commons.model.cache +import com.vitorpamplona.amethyst.commons.model.IAccount import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.search.RenderableKinds -import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.tagValueContains import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -57,11 +56,11 @@ class CacheSearch( ) { fun findUsersStartingWith( username: String, - forAccount: Account?, + forAccount: IAccount?, ): List { if (username.isBlank()) return emptyList() - checkNotInMainThread() + cache.appHost.assertNotMainThread() val key = decodePublicKeyAsHexOrNull(username) @@ -89,11 +88,12 @@ class CacheSearch( user.pubkeyHex.startsWith(username, true) || user.pubkeyNpub().startsWith(username, true) ) && - (forAccount == null || (!forAccount.isHidden(user) && !metadata.anyPropertyContains(forAccount.hiddenUsers.flow.value.hiddenWordsCase))) + (forAccount == null || (!forAccount.isHidden(user) && !metadata.anyPropertyContains(forAccount.hiddenWordsCase))) } } - val findsFollowing = finds.associateWith { forAccount?.isFollowing(it) == true } + val following = forAccount?.followingKeySet() ?: emptySet() + val findsFollowing = finds.associateWith { it.pubkeyHex in following } val anyNameStartsWith = finds.associateWith { it.metadataOrNull()?.anyNameStartsWith(dualCase) == true } val anyAddressStartsWith = finds.associateWith { it.metadataOrNull()?.anyAddressStartsWith(dualCase) == true } val displayNames = finds.associateWith { it.toBestDisplayName().lowercase() } @@ -152,7 +152,7 @@ class CacheSearch( filters: List, hidden: LiveHiddenUsers, ): List { - checkNotInMainThread() + cache.appHost.assertNotMainThread() if (filters.isEmpty()) return emptyList() @@ -189,7 +189,7 @@ class CacheSearch( text: String, hidden: LiveHiddenUsers, ): List { - checkNotInMainThread() + cache.appHost.assertNotMainThread() if (text.isBlank()) return emptyList() diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCacheAddressExt.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCacheAddressExt.kt index 052642f80f..5ba77446a2 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCacheAddressExt.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCacheAddressExt.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.commons.model.cache import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.cache.LargeSoftCache import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.utils.cache.CacheCollectors diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCache.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCache.kt index ebf639ff2b..5f4adbaf49 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCache.kt @@ -20,10 +20,9 @@ */ @file:Suppress("DEPRECATION") -package com.vitorpamplona.amethyst.model +package com.vitorpamplona.amethyst.commons.model.cache import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.cashu.MintDirectoryIndex import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Channel @@ -40,9 +39,7 @@ import com.vitorpamplona.amethyst.commons.model.buzz.BuzzPresenceState import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect import com.vitorpamplona.amethyst.commons.model.buzz.BuzzTypingState import com.vitorpamplona.amethyst.commons.model.buzz.BuzzWorkspaceStates -import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider -import com.vitorpamplona.amethyst.commons.model.cache.LargeSoftCache -import com.vitorpamplona.amethyst.commons.model.cache.filter +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache.observeEvents import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel @@ -51,6 +48,8 @@ import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChann import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupDeletions import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.model.nip88Polls.PollTallyPolicy +import com.vitorpamplona.amethyst.commons.model.nip90DVMs.DvmHeartbeatRegistry +import com.vitorpamplona.amethyst.commons.model.nipBCOnchainZaps.OnchainZapResolver import com.vitorpamplona.amethyst.commons.model.observables.CreatedAtIdHexComparator import com.vitorpamplona.amethyst.commons.model.observables.EventListMatchingFilter import com.vitorpamplona.amethyst.commons.model.observables.NewEventMatchingFilter @@ -60,11 +59,7 @@ import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList import com.vitorpamplona.amethyst.commons.model.redirectStrayRelayGroupContent import com.vitorpamplona.amethyst.commons.service.BundledInsert import com.vitorpamplona.amethyst.commons.service.nwc.NwcPaymentTracker -import com.vitorpamplona.amethyst.isDebug -import com.vitorpamplona.amethyst.model.LocalCache.observeEvents -import com.vitorpamplona.amethyst.model.nipBCOnchainZaps.OnchainZapResolver -import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.ui.note.dateFormatter +import com.vitorpamplona.amethyst.commons.util.dateFormatter import com.vitorpamplona.quartz.buzz.aeEngrams.EngramEvent import com.vitorpamplona.quartz.buzz.agentProfiles.AgentProfileEvent import com.vitorpamplona.quartz.buzz.amTurnMetrics.AgentTurnMetricEvent @@ -433,17 +428,17 @@ import java.io.FileOutputStream import java.io.IOException import java.util.SortedSet -interface ILocalCache { - fun markAsSeen( - eventId: String, - relay: NormalizedRelayUrl, - ) { - // Default no-op; implementations may override to track seen events per relay - } -} - object LocalCache : ILocalCache, ICacheProvider, Dao { - val antiSpam = AntiSpamFilter() + /** + * What this cache needs from the application shell around it: a scope to verify on, the + * NIP-95 blob directory, the relay identity and stats sinks, and the main-thread assertion. + * Defaults to [LocalCacheHost.Default], which supplies none of them; Android installs its + * own during app startup. + */ + @Volatile + var appHost: LocalCacheHost = LocalCacheHost + + val antiSpam = AntiSpamFilter { appHost } val users = LargeSoftCache() val notes = LargeSoftCache() @@ -2135,10 +2130,7 @@ object LocalCache : ILocalCache, ICacheProvider, Dao { event: Event, relay: NormalizedRelayUrl, ): Boolean { - val self = - Amethyst.instance.nip11Cache - .getFromCache(relay) - .self ?: return true + val self = appHost.relaySelfPubKey(relay) ?: return true return event.pubKey == self } @@ -2716,7 +2708,7 @@ object LocalCache : ILocalCache, ICacheProvider, Dao { attachZapToLiveActivityChannel(event, note, relay) refreshNewNoteObservers(note) - Amethyst.instance.applicationIOScope.launch { + appHost.scope.launch { try { val info = resolver.resolve(recipientLnurlpUrl) if (info == null) { @@ -3078,37 +3070,48 @@ object LocalCache : ILocalCache, ICacheProvider, Dao { note.addRelay(relay) } + // A host with no blob directory keeps the bytes in memory below instead of spilling them. + val cachePath = appHost.nip95BlobDir + val isVerified = - try { - val cachePath = Amethyst.instance.nip95cache - cachePath.mkdirs() - val file = File(cachePath, event.id) - if (!file.exists() && (wasVerified || justVerify(event))) { - FileOutputStream(file).use { stream -> - stream.write(event.decode()) + if (cachePath == null) { + wasVerified + } else { + try { + cachePath.mkdirs() + val file = File(cachePath, event.id) + if (!file.exists() && (wasVerified || justVerify(event))) { + FileOutputStream(file).use { stream -> + stream.write(event.decode()) + } + Log.i( + "FileStorageEvent", + "NIP95 File received from $relay and saved to disk as $file", + ) + true + } else { + wasVerified } - Log.i( - "FileStorageEvent", - "NIP95 File received from $relay and saved to disk as $file", - ) - true - } else { + } catch (e: IOException) { + Log.e("FileStorageEvent", "FileStorageEvent save to disk error: " + event.id, e) wasVerified } - } catch (e: IOException) { - Log.e("FileStorageEvent", "FileStorageEvent save to disk error: " + event.id, e) - wasVerified } // Already processed this event. if (note.event != null) return false if (isVerified || justVerify(event)) { - // this is an invalid event. But we don't need to keep the data in memory. - val eventNoData = - FileStorageEvent(event.id, event.pubKey, event.createdAt, event.tags, "", event.sig) + // The blob is on disk now, so the copy in the note drops its content. Without a + // directory to spill to there is nowhere else for it to live, so keep it whole. + val stored = + if (cachePath != null) { + FileStorageEvent(event.id, event.pubKey, event.createdAt, event.tags, "", event.sig) + } else { + event + } - note.loadEvent(eventNoData, author, emptyList()) + note.loadEvent(stored, author, emptyList()) refreshNewNoteObservers(note) @@ -3228,7 +3231,7 @@ object LocalCache : ILocalCache, ICacheProvider, Dao { requestNote?.let { request -> zappedNote?.addZapPayment(request, note) } - Amethyst.instance.applicationIOScope.launch { + appHost.scope.launch { responseCallback(event) } @@ -3309,7 +3312,7 @@ object LocalCache : ILocalCache, ICacheProvider, Dao { var verifyMeter: ((elapsedNanos: Long, valid: Boolean) -> Unit)? = null fun justVerify(event: Event): Boolean { - checkNotInMainThread() + appHost.assertNotMainThread() val meter = verifyMeter if (meter == null) return justVerifyInner(event) @@ -3356,7 +3359,7 @@ object LocalCache : ILocalCache, ICacheProvider, Dao { deletionIndex.hasBeenDeletedBy(event)?.let { deletionEvent -> getNoteIfExists(deletionEvent.id)?.let { note -> if (!note.hasRelay(relay.url)) { - if (isDebug) { + if (appHost.isDebug) { Log.d("LocalCache") { "Updating ${relay.url.url} with a Deletion Event ${event.id} ${deletionEvent.id} because of ${event.toJson()} with ${deletionEvent.toJson()}" } } relay.sendIfConnected(EventCmd(deletionEvent)) @@ -3373,7 +3376,7 @@ object LocalCache : ILocalCache, ICacheProvider, Dao { getAddressableNoteIfExists(event.address())?.let { note -> note.event?.let { existingEvent -> if (existingEvent.createdAt > event.createdAt && !note.hasRelay(relay.url) && !deletionIndex.hasBeenDeleted(event) && !event.isExpired()) { - if (isDebug) { + if (appHost.isDebug) { Log.d("LocalCache") { "Updating ${relay.url.url} with a new version of ${event.kind} ${event.id} to ${existingEvent.id}" } } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt new file mode 100644 index 0000000000..ba9d6814e4 --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.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.commons.model.cache + +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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import java.io.File + +/** + * The handful of things the event cache needs from the application shell around it. + * + * The cache itself is a plain in-memory store and can run without any of them, so every + * member here has a default that is the honest answer for a host that does not provide it + * — an own scope, no NIP-95 spill directory, no relay identity, no stats sink. A front end + * installs its own via [LocalCache.host]; Android's `Amethyst` does so at startup, Desktop + * and unit tests keep the defaults. + * + * This exists so [LocalCache] can live in `commons` without naming `Amethyst.instance`, + * `BuildConfig` or `android.os.Looper`. + */ +interface LocalCacheHost { + /** Scope for the cache's own background work: NIP-BC chain lookups and LNURL fetches. */ + val scope: CoroutineScope + + /** + * Whether this is a debug build. Gates the two very chatty per-event log lines that + * dump full event JSON while back-filling a relay with a deletion or a newer version. + */ + val isDebug: Boolean + + /** + * Directory the NIP-95 `FileStorageEvent` blobs are spilled to, so their bytes can leave + * memory once they are on disk. `null` keeps the event in memory and skips the spill. + */ + val nip95BlobDir: File? + + /** Per-relay counters the anti-spam filter reports duplicate events to. */ + val relayStats: RelayStats? + + /** + * The relay's own pubkey, as its NIP-11 document advertises it (`self`), used to accept + * relay-signed NIP-29 group metadata. `null` means unknown, which accepts the event. + */ + fun relaySelfPubKey(relay: NormalizedRelayUrl): HexKey? = null + + /** + * Throws if called on the platform's main thread. Signature verification and the cache + * sweeps are far too slow to run there. A no-op by default and on release builds. + */ + fun assertNotMainThread() {} + + /** The neutral host: an own IO scope and nothing else. */ + companion object Default : LocalCacheHost { + override val scope: CoroutineScope by lazy { CoroutineScope(Dispatchers.IO + SupervisorJob()) } + override val isDebug = false + override val nip95BlobDir: File? = null + override val relayStats: RelayStats? = null + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/DvmHeartbeatRegistry.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip90DVMs/DvmHeartbeatRegistry.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/DvmHeartbeatRegistry.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip90DVMs/DvmHeartbeatRegistry.kt index 5f7ec90245..ab7f5e4d45 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/DvmHeartbeatRegistry.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip90DVMs/DvmHeartbeatRegistry.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 +package com.vitorpamplona.amethyst.commons.model.nip90DVMs import com.vitorpamplona.quartz.nip01Core.core.Address import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipBCOnchainZaps/OnchainZapResolver.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipBCOnchainZaps/OnchainZapResolver.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt index b22c441921..ccee686507 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipBCOnchainZaps/OnchainZapResolver.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.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.amethyst.model.nipBCOnchainZaps +package com.vitorpamplona.amethyst.commons.model.nipBCOnchainZaps -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.OnchainZapStatus -import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nipBCOnchainZaps.verify.OnchainZapVerifier import com.vitorpamplona.quartz.nipBCOnchainZaps.verify.VerifiedOnchainZap @@ -33,10 +32,8 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -94,16 +91,11 @@ class OnchainZapResolver( * entries whenever the tip advances. Lazy + `WhileSubscribed` so the HTTP call * only fires when at least one UI surface needs it. * - * Lazy initialization is required because [Amethyst.instance] may not exist - * when [OnchainZapResolver] is first constructed. Falls back to a constant - * null-emitting [StateFlow] if the application scope isn't available yet - * (e.g. unit tests, ContentProvider invocations), so the lazy field doesn't - * permanently fail with `UninitializedPropertyAccessException`. + * Lazy so the scope is read from the cache's host after the app shell has installed + * it, rather than at construction time — the cache is a singleton and is built first. */ val onchainTipHeightFlow: StateFlow by lazy { - val scope = - runCatching { Amethyst.instance.applicationIOScope }.getOrNull() - ?: return@lazy MutableStateFlow(null).asStateFlow() + val scope = cache.appHost.scope flow { while (true) { @@ -150,7 +142,7 @@ class OnchainZapResolver( val verifier = OnchainZapVerifier(backend) - Amethyst.instance.applicationIOScope.launch { + cache.appHost.scope.launch { try { verifyAndUpgradeOnchainZap(event, source, repliesTo, verifier) } finally { From 9d18ab83dd8f87eea0386de7114067c78a91db46 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 16:25:01 +0000 Subject: [PATCH 02/29] docs(commons): record where the event cache lives after the move MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `model/cache` no longer holds only interfaces, and the reason it is `jvmAndroid` rather than `commonMain` — the NIP-95 blob spill through `java.io.File` — is worth writing down next to what would free it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- commons/ARCHITECTURE.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/commons/ARCHITECTURE.md b/commons/ARCHITECTURE.md index bda85c11a5..b28e0bee01 100644 --- a/commons/ARCHITECTURE.md +++ b/commons/ARCHITECTURE.md @@ -75,7 +75,7 @@ in `commonsUI`, under the same package. ### Domain models & data | Package | UI? | Purpose | |----------------|-----|---------| -| `model` | no¹ | Core domain types (`Note`, `User`, `Channel`), thread assembly (`ThreadAssembler`, `ThreadLevelCalculator`, `ReplyContext`, `replyingDirectlyTo`), and per-NIP event model extensions in `model/nipNN…` subpackages. `model/cache` holds the in-memory event-store interfaces + `UserMetadataCache`. `model/account`, `model/observables`. The largest package; keep it organized by NIP. | +| `model` | no¹ | Core domain types (`Note`, `User`, `Channel`), thread assembly (`ThreadAssembler`, `ThreadLevelCalculator`, `ReplyContext`, `replyingDirectlyTo`), and per-NIP event model extensions in `model/nipNN…` subpackages. `model/cache` holds the in-memory event store: the `ICacheProvider` / `ILocalCache` ports and `UserMetadataCache` in commonMain, and the concrete `LocalCache` (plus `AntiSpamFilter`, `CachePruner`, `CacheSearch` and the `LocalCacheHost` app-shell port) in jvmAndroid. `model/account`, `model/observables`. The largest package; keep it organized by NIP. | | `defaults` | no | Static bootstrap data (default relays, channels). | ¹ `model` uses only the `@Stable`/`@Immutable` runtime annotations — CLI-safe. @@ -253,6 +253,10 @@ These are intentionally *documented*, not silently tolerated. Fix opportunistica namespaces expected to grow; do not fold them into `util` just for size. - **`onchain`** (on-chain zap splitting) is `quartz`-adjacent but un-numbered; leave readable unless a clear NIP number lands. +- **`model/cache/LocalCache` is `jvmAndroid`, not `commonMain`**, because it + spills NIP-95 blobs through `java.io.File`. Promoting it needs an okio (or + `expect`) sink behind `LocalCacheHost.nip95BlobDir`; nothing else in the + move-group blocks iOS. --- From 99087ffa8e7f552668beb006d5800f2929ac4e62 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 16:49:34 +0000 Subject: [PATCH 03/29] refactor(commons): make the event cache a class, and give it the user search the port declares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two prerequisites for retiring `DesktopLocalCache`, both of which stand on their own. `object LocalCache` becomes `open class EventCache` plus `object LocalCache : EventCache()`. Desktop's cache is a class and 68 of its 69 constructions are in tests, each taking an isolated cache; a process-wide singleton would take that isolation away. Android reaches the cache statically from ~350 call sites, so the singleton keeps the name and none of them change — including `import LocalCache.observeEvents`, which resolves fine against an inherited member. `CachePruner`, `CacheSearch` and `OnchainZapResolver` now take the class, and the two `this@LocalCache::filter` callback labels become `this@EventCache`. `ICacheProvider.findUsersStartingWith(prefix, limit)` defaults to an empty list, and `EventCache` never overrode it — `CacheSearch` has the search but under a different signature (`username, forAccount`, no limit). Anything in commons asking the port for users therefore got nothing from this cache; `DesktopLocalCache` overrode it, which is why `UserSearchEngine` and `SearchBarState` work there and why swapping them over would have silently emptied Desktop's user search. `CacheSearch.findUsersStartingWith` grows an optional `limit` — applied after ranking, so the cap never costs the best matches — and `EventCache` overrides the port method through it. With a null account the two rankings are identical: the following-first key is constant when nothing is followed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../commons/model/cache/CachePruner.kt | 2 +- .../commons/model/cache/CacheSearch.kt | 32 +- .../commons/model/cache/EventCache.kt | 4038 +++++++++++++++++ .../commons/model/cache/LocalCache.kt | 4014 +--------------- .../nipBCOnchainZaps/OnchainZapResolver.kt | 4 +- 5 files changed, 4075 insertions(+), 4015 deletions(-) create mode 100644 commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt index bf37e2c96a..714cf8998f 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt @@ -62,7 +62,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils * `MemoryTrimmingService`. */ class CachePruner( - private val cache: LocalCache, + private val cache: EventCache, ) { fun cleanMemory() { Log.d("LargeCache") { "Notes cleanup started. Current size: ${cache.notes.size()}" } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt index 7c9d692797..6abcdaefd3 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt @@ -52,11 +52,20 @@ import kotlinx.coroutines.CancellationException * tested against a populated cache. */ class CacheSearch( - private val cache: LocalCache, + private val cache: EventCache, ) { + /** + * Users whose name, display name, NIP-05 or lightning address matches [username], best first. + * + * [forAccount] is the reader: when given, their muted users and muted words are filtered out + * and the people they follow sort first. `null` searches everything and ranks on the name + * match alone — what a caller with no account in hand (a mention autocomplete, a spotlight) + * wants. [limit] caps the result *after* ranking, so the cap never costs the best matches. + */ fun findUsersStartingWith( username: String, forAccount: IAccount?, + limit: Int? = null, ): List { if (username.isBlank()) return emptyList() @@ -98,15 +107,18 @@ class CacheSearch( val anyAddressStartsWith = finds.associateWith { it.metadataOrNull()?.anyAddressStartsWith(dualCase) == true } val displayNames = finds.associateWith { it.toBestDisplayName().lowercase() } - return finds.sortedWith( - compareBy( - { findsFollowing[it] == false }, - { anyNameStartsWith[it] == false }, - { anyAddressStartsWith[it] == false }, - { displayNames[it] }, - { it.pubkeyHex }, - ), - ) + val ranked = + finds.sortedWith( + compareBy( + { findsFollowing[it] == false }, + { anyNameStartsWith[it] == false }, + { anyAddressStartsWith[it] == false }, + { displayNames[it] }, + { it.pubkeyHex }, + ), + ) + + return if (limit != null) ranked.take(limit) else ranked } /** diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt new file mode 100644 index 0000000000..5028446fe3 --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt @@ -0,0 +1,4038 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +@file:Suppress("DEPRECATION") + +package com.vitorpamplona.amethyst.commons.model.cache + +import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.cashu.MintDirectoryIndex +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.Dao +import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.OnchainZapStatus +import com.vitorpamplona.amethyst.commons.model.RelayGroupTargetCandidate +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.UserContext +import com.vitorpamplona.amethyst.commons.model.buzz.BuzzCommunityMembership +import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmRegistry +import com.vitorpamplona.amethyst.commons.model.buzz.BuzzPresenceState +import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect +import com.vitorpamplona.amethyst.commons.model.buzz.BuzzTypingState +import com.vitorpamplona.amethyst.commons.model.buzz.BuzzWorkspaceStates +import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel +import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupDeletions +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip88Polls.PollTallyPolicy +import com.vitorpamplona.amethyst.commons.model.nip90DVMs.DvmHeartbeatRegistry +import com.vitorpamplona.amethyst.commons.model.nipBCOnchainZaps.OnchainZapResolver +import com.vitorpamplona.amethyst.commons.model.observables.CreatedAtIdHexComparator +import com.vitorpamplona.amethyst.commons.model.observables.EventListMatchingFilter +import com.vitorpamplona.amethyst.commons.model.observables.NewEventMatchingFilter +import com.vitorpamplona.amethyst.commons.model.observables.NoteListMatchingFilter +import com.vitorpamplona.amethyst.commons.model.observables.Observable +import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList +import com.vitorpamplona.amethyst.commons.model.redirectStrayRelayGroupContent +import com.vitorpamplona.amethyst.commons.service.BundledInsert +import com.vitorpamplona.amethyst.commons.service.nwc.NwcPaymentTracker +import com.vitorpamplona.amethyst.commons.util.dateFormatter +import com.vitorpamplona.quartz.buzz.aeEngrams.EngramEvent +import com.vitorpamplona.quartz.buzz.agentProfiles.AgentProfileEvent +import com.vitorpamplona.quartz.buzz.amTurnMetrics.AgentTurnMetricEvent +import com.vitorpamplona.quartz.buzz.aoObserver.ObserverFrameEvent +import com.vitorpamplona.quartz.buzz.apPersonas.PersonaEvent +import com.vitorpamplona.quartz.buzz.audit.AuditEntryEvent +import com.vitorpamplona.quartz.buzz.cwChannelWindow.WindowBoundsEvent +import com.vitorpamplona.quartz.buzz.dm.DmAddMemberEvent +import com.vitorpamplona.quartz.buzz.dm.DmCreatedEvent +import com.vitorpamplona.quartz.buzz.dm.DmHideEvent +import com.vitorpamplona.quartz.buzz.dm.DmOpenEvent +import com.vitorpamplona.quartz.buzz.dvDmVisibility.DmVisibilityEvent +import com.vitorpamplona.quartz.buzz.erReminders.EventReminderEvent +import com.vitorpamplona.quartz.buzz.forum.ForumCommentEvent +import com.vitorpamplona.quartz.buzz.forum.ForumPostEvent +import com.vitorpamplona.quartz.buzz.forum.ForumVoteEvent +import com.vitorpamplona.quartz.buzz.huddles.HuddleEndedEvent +import com.vitorpamplona.quartz.buzz.huddles.HuddleGuidelinesEvent +import com.vitorpamplona.quartz.buzz.huddles.HuddleParticipantJoinedEvent +import com.vitorpamplona.quartz.buzz.huddles.HuddleParticipantLeftEvent +import com.vitorpamplona.quartz.buzz.huddles.HuddleReactionEvent +import com.vitorpamplona.quartz.buzz.huddles.HuddleStartedEvent +import com.vitorpamplona.quartz.buzz.iaIdentityArchival.ArchiveRequestEvent +import com.vitorpamplona.quartz.buzz.iaIdentityArchival.ArchivedIdentitiesListEvent +import com.vitorpamplona.quartz.buzz.iaIdentityArchival.ArchivedIdentityEvent +import com.vitorpamplona.quartz.buzz.iaIdentityArchival.UnarchiveRequestEvent +import com.vitorpamplona.quartz.buzz.iaIdentityArchival.UnarchivedIdentityEvent +import com.vitorpamplona.quartz.buzz.jobs.JobAcceptedEvent +import com.vitorpamplona.quartz.buzz.jobs.JobCancelEvent +import com.vitorpamplona.quartz.buzz.jobs.JobErrorEvent +import com.vitorpamplona.quartz.buzz.jobs.JobProgressEvent +import com.vitorpamplona.quartz.buzz.jobs.JobRequestEvent +import com.vitorpamplona.quartz.buzz.jobs.JobResultEvent +import com.vitorpamplona.quartz.buzz.managedAgents.ManagedAgentEvent +import com.vitorpamplona.quartz.buzz.moderation.ModerationBanEvent +import com.vitorpamplona.quartz.buzz.moderation.ModerationResolveReportEvent +import com.vitorpamplona.quartz.buzz.moderation.ModerationTimeoutEvent +import com.vitorpamplona.quartz.buzz.moderation.ModerationUntimeoutEvent +import com.vitorpamplona.quartz.buzz.moderation.ProductFeedbackEvent +import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent +import com.vitorpamplona.quartz.buzz.notifications.MemberRemovedNotificationEvent +import com.vitorpamplona.quartz.buzz.pairing.PairingEvent +import com.vitorpamplona.quartz.buzz.plPushLease.PushLeaseEvent +import com.vitorpamplona.quartz.buzz.presence.PresenceUpdateEvent +import com.vitorpamplona.quartz.buzz.presence.TypingIndicatorEvent +import com.vitorpamplona.quartz.buzz.relayAdmin.RelayAdminAddMemberEvent +import com.vitorpamplona.quartz.buzz.relayAdmin.RelayAdminChangeRoleEvent +import com.vitorpamplona.quartz.buzz.relayAdmin.RelayAdminRemoveMemberEvent +import com.vitorpamplona.quartz.buzz.stream.CanvasEvent +import com.vitorpamplona.quartz.buzz.stream.StreamMessageBookmarkedEvent +import com.vitorpamplona.quartz.buzz.stream.StreamMessageDiffEvent +import com.vitorpamplona.quartz.buzz.stream.StreamMessageEditEvent +import com.vitorpamplona.quartz.buzz.stream.StreamMessagePinnedEvent +import com.vitorpamplona.quartz.buzz.stream.StreamMessageScheduledEvent +import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event +import com.vitorpamplona.quartz.buzz.stream.StreamReminderEvent +import com.vitorpamplona.quartz.buzz.stream.SystemMessageEvent +import com.vitorpamplona.quartz.buzz.stream.SystemMessagePayload +import com.vitorpamplona.quartz.buzz.stream.sidecars.ChannelSummaryEvent +import com.vitorpamplona.quartz.buzz.stream.sidecars.PresenceSnapshotEvent +import com.vitorpamplona.quartz.buzz.teams.TeamEvent +import com.vitorpamplona.quartz.buzz.threading.buzzThreadReply +import com.vitorpamplona.quartz.buzz.workflow.ApprovalDenyEvent +import com.vitorpamplona.quartz.buzz.workflow.ApprovalGrantEvent +import com.vitorpamplona.quartz.buzz.workflow.WorkflowApprovalDeniedEvent +import com.vitorpamplona.quartz.buzz.workflow.WorkflowApprovalGrantedEvent +import com.vitorpamplona.quartz.buzz.workflow.WorkflowApprovalRequestedEvent +import com.vitorpamplona.quartz.buzz.workflow.WorkflowCancelledEvent +import com.vitorpamplona.quartz.buzz.workflow.WorkflowCompletedEvent +import com.vitorpamplona.quartz.buzz.workflow.WorkflowDefEvent +import com.vitorpamplona.quartz.buzz.workflow.WorkflowFailedEvent +import com.vitorpamplona.quartz.buzz.workflow.WorkflowStepCompletedEvent +import com.vitorpamplona.quartz.buzz.workflow.WorkflowStepFailedEvent +import com.vitorpamplona.quartz.buzz.workflow.WorkflowStepStartedEvent +import com.vitorpamplona.quartz.buzz.workflow.WorkflowTriggerEvent +import com.vitorpamplona.quartz.buzz.workflow.WorkflowTriggeredEvent +import com.vitorpamplona.quartz.buzz.wpWorkspaceProfile.SetWorkspaceProfileEvent +import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent +import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId +import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChatEditEvent +import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent +import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent +import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent +import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent +import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent +import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent +import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChatEvent +import com.vitorpamplona.quartz.experimental.citations.ExternalCitationEvent +import com.vitorpamplona.quartz.experimental.citations.HardcopyCitationEvent +import com.vitorpamplona.quartz.experimental.citations.PromptCitationEvent +import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent +import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent +import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId +import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent +import com.vitorpamplona.quartz.experimental.fitness.workout.ExerciseTemplateEvent +import com.vitorpamplona.quartz.experimental.fitness.workout.WorkoutRecordEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent +import com.vitorpamplona.quartz.experimental.library.BlossomPieceIndexEvent +import com.vitorpamplona.quartz.experimental.library.BookshelfDirectoryEvent +import com.vitorpamplona.quartz.experimental.library.LearningResourceEvent +import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent +import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent +import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent +import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent +import com.vitorpamplona.quartz.experimental.nns.NNSEvent +import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent +import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent +import com.vitorpamplona.quartz.experimental.ps1saves.Ps1SaveEvent +import com.vitorpamplona.quartz.experimental.publications.PublicationContentEvent +import com.vitorpamplona.quartz.experimental.publications.PublicationIndexEvent +import com.vitorpamplona.quartz.experimental.ratings.EntityRatingEvent +import com.vitorpamplona.quartz.experimental.ratings.RelayReviewEvent +import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent +import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent +import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent +import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent +import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent +import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent +import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent +import com.vitorpamplona.quartz.nip01Core.core.Address +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.isAddressable +import com.vitorpamplona.quartz.nip01Core.core.isRegular +import com.vitorpamplona.quartz.nip01Core.core.isReplaceable +import com.vitorpamplona.quartz.nip01Core.crypto.checkSignature +import com.vitorpamplona.quartz.nip01Core.crypto.verify +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.commands.toRelay.EventCmd +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterIndex +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag +import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents +import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionIndex +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 +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip18Reposts.BaseRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip18Reposts.quotes.taggedQuoteIds +import com.vitorpamplona.quartz.nip19Bech32.isATag +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip25Reactions.ExternalReactionEvent +import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelHideMessageEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMuteUserEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip29RelayGroups.GroupId +import com.vitorpamplona.quartz.nip29RelayGroups.groupId +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupParticipantsEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupPinnedEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.SupportedRolesEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateGroupEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateInviteEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteEventEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteGroupEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.PutUserEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.RemoveUserEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.UpdatePinListEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.JoinRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.LeaveRequestEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip32Labeling.LabelEvent +import com.vitorpamplona.quartz.nip34Git.grasp.UserGraspListEvent +import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent +import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestEvent +import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestUpdateEvent +import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent +import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent +import com.vitorpamplona.quartz.nip34Git.state.GitRepositoryStateEvent +import com.vitorpamplona.quartz.nip34Git.status.GitStatusEvent +import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent +import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent +import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent +import com.vitorpamplona.quartz.nip40Expiration.isExpired +import com.vitorpamplona.quartz.nip43RelayMembers.addMember.RelayAddMemberEvent +import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent +import com.vitorpamplona.quartz.nip43RelayMembers.removeMember.RelayRemoveMemberEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.PinListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent +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.interestSet.InterestSetEvent +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent +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.ProxyRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent +import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent +import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent +import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent +import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent +import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent +import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent +import com.vitorpamplona.quartz.nip53LiveActivities.nestsServers.NestsServersEvent +import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent +import com.vitorpamplona.quartz.nip53LiveActivities.raid.LiveActivitiesRaidEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiMergeAcceptanceEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiMergeRequestEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiRedirectEvent +import com.vitorpamplona.quartz.nip56Reports.ReportEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.validate.LnZapReceiptValidator +import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointCache +import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointResolver +import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlForm +import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent +import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent +import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent +import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent +import com.vitorpamplona.quartz.nip59Giftwrap.HasInnerEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent +import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent +import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent +import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent +import com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent +import com.vitorpamplona.quartz.nip60Cashu.quote.CashuMintQuoteEvent +import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent +import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletEvent +import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent +import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent +import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.claimedSatsTotal +import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent +import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent +import com.vitorpamplona.quartz.nip64Chess.challenge.offer.LiveChessGameChallengeEvent +import com.vitorpamplona.quartz.nip64Chess.draw.LiveChessDrawOfferEvent +import com.vitorpamplona.quartz.nip64Chess.end.LiveChessGameEndEvent +import com.vitorpamplona.quartz.nip64Chess.game.ChessGameEvent +import com.vitorpamplona.quartz.nip64Chess.jester.JesterEvent +import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.RelayDiscoveryEvent +import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.RelayMonitorEvent +import com.vitorpamplona.quartz.nip68Picture.PictureEvent +import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent +import com.vitorpamplona.quartz.nip71Video.VideoShortEvent +import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent +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.nip7DThreads.ThreadEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent +import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent +import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent +import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent +import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.dvmHeartbeat.DvmHeartbeatEvent +import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent +import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryRequest.NIP90UserDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryResponse.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.nipA4PublicMessages.PublicMessageEvent +import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallAnswerEvent +import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallHangupEvent +import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallIceCandidateEvent +import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent +import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRejectEvent +import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRenegotiateEvent +import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent +import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListEvent +import com.vitorpamplona.quartz.nipB1Bolt12Zaps.verify.Bolt12ZapValidation +import com.vitorpamplona.quartz.nipB1Bolt12Zaps.verify.Bolt12ZapValidator +import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent +import com.vitorpamplona.quartz.nipBCOnchainZaps.chain.OnchainBackend +import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent +import com.vitorpamplona.quartz.nipC0CodeSnippets.CodeSnippetEvent +import com.vitorpamplona.quartz.nipC7Chats.ChatEvent +import com.vitorpamplona.quartz.nipF4Podcasts.authored.AuthoredPodcastsEvent +import com.vitorpamplona.quartz.nipF4Podcasts.episode.PodcastEpisodeEvent +import com.vitorpamplona.quartz.nipF4Podcasts.favorites.FavoritePodcastsListEvent +import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent +import com.vitorpamplona.quartz.nipXXPodcasting20.episode.Podcasting20EpisodeEvent +import com.vitorpamplona.quartz.nipXXPodcasting20.trailer.Podcasting20TrailerEvent +import com.vitorpamplona.quartz.utils.Hex +import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.cache.LargeCache +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.SortedSet + +/** + * The in-memory event store: every `Note`, `User` and `Channel` the app has consumed, plus the + * indexes over them. + * + * A class rather than a singleton so a front end can own its cache and a test can have one to + * itself. Android uses the process-wide [LocalCache] instance; Desktop and tests build their own. + */ +open class EventCache : + ILocalCache, + ICacheProvider, + Dao { + /** + * What this cache needs from the application shell around it: a scope to verify on, the + * NIP-95 blob directory, the relay identity and stats sinks, and the main-thread assertion. + * Defaults to [LocalCacheHost.Default], which supplies none of them; Android installs its + * own during app startup. + */ + @Volatile + var appHost: LocalCacheHost = LocalCacheHost + + val antiSpam = AntiSpamFilter { appHost } + + val users = LargeSoftCache() + val notes = LargeSoftCache() + val addressables = LargeSoftCache() + + val chatroomList = LargeCache() + val publicChatChannels = LargeCache() + val liveChatChannels = LargeCache() + val ephemeralChannels = LargeCache() + val geohashChannels = LargeCache() + val relayGroupChannels = LargeCache() + val concordChannels = LargeCache() + + val paymentTracker = NwcPaymentTracker() + + /** + * Bitcoin chain backend used by [consume]`(OnchainZapEvent)` to verify NIP-BC zaps + * against the actual on-chain transaction. `null` disables verification (incoming + * onchain zap events still get cached, but no `Note.zapsAmount` contribution). + * Set during account init. + */ + @Volatile + var onchainBackend: OnchainBackend? = null + + /** + * NIP-BC on-chain zap verification coordinator. Owns the chain-tip poller flow, + * the in-flight de-duplication of verifier calls across consume/reverify paths, + * and the parallelism cap. `consume(OnchainZapEvent)` delegates the async + * verification side here; the gallery's reverification driver also calls in here + * directly. + */ + val onchainZapResolver = OnchainZapResolver(this) + + /** + * NIP-B1 BOLT12 zap validator. Unlike onchain zaps, BOLT12 proof verification + * is synchronous (a self-contained `lnp` payer proof), so `consume(Bolt12ZapEvent)` + * validates inline and needs no async resolver. + */ + val bolt12ZapValidator = Bolt12ZapValidator() + + /** + * Resolver for LNURL provider metadata used by [consume]`(LnZapEvent)` to + * validate NIP-57 Appendix F. `null` skips the receipt-signer check (the + * receipt is still accepted on signature verification alone — matches legacy + * behavior); set this in app init so receipts can be verified against the + * recipient's provider's advertised `nostrPubkey`. + */ + @Volatile + var lnurlEndpointResolver: LnurlEndpointResolver? = null + + override val relayHints = HintIndexer() + + /** + * Cashu mint URL directory, populated passively as + * `NutzapInfoEvent` / `MintRecommendationEvent` / `CashuMintEvent` + * flow through `updateMintIndex`. Backs the autocomplete in any text + * field where the user types a mint URL. + * + * Use [mintDirectoryBackfilled] semantics via [ensureMintDirectoryBackfilled] + * when reading from the UI — the first call sweeps already-cached + * events so suggestions are useful before the next relay round-trip. + */ + val mintDirectory = MintDirectoryIndex() + + @Volatile private var mintDirectoryBackfilled = false + private val mintDirectoryBackfillLock = Any() + + /** + * Sweeps `notes` + `addressables` for any NIP-87 / NIP-61 event the + * cache already holds and feeds them into [mintDirectory]. The wallet + * directory + every other user's kind:10019 we've ever loaded may + * have arrived BEFORE [updateMintIndex] existed (or before any + * caller cared), so without this backfill the autocomplete is empty + * until new events arrive. + * + * Idempotent — subsequent calls return immediately. Best-effort: a + * scan failure is swallowed so the index stays usable even if the + * cache is in an unexpected state. + */ + fun ensureMintDirectoryBackfilled() { + if (mintDirectoryBackfilled) return + synchronized(mintDirectoryBackfillLock) { + if (mintDirectoryBackfilled) return + runCatching { + notes.forEach { _, note -> note.event?.let(::updateMintIndex) } + addressables.forEach { _, note -> note.event?.let(::updateMintIndex) } + } + mintDirectoryBackfilled = true + } + } + + val deletionIndex = DeletionIndex() + + /** + * Inverted index over the active [Observable]s. New events fan + * out only to observers whose filter actually narrows on a field + * the event carries (author, kind, single-letter tag), instead + * of waking every observer per event. Predicate-only observers + * (no underlying [Filter]) live in the unindexed pool and still + * see every event. + */ + val observables = FilterIndex() + + /** + * Memory-reclaim policy (soft-cache trims + hidden/old/expired/superseded event + * pruning) and the shared [CachePruner.unlinkAndRemove] removal primitive. + */ + val pruner = CachePruner(this) + + /** Prefix/content search over users, notes, and channels. */ + val search = CacheSearch(this) + + // The search entry points of ICacheProvider, so a shared state holder can ask a cache what it + // holds without naming this module. The policy stays in CacheSearch; these are the door. + + override fun findNotesMatching( + filters: List, + hidden: LiveHiddenUsers, + ) = search.findNotesMatching(filters, hidden) + + override fun findNotesStartingWith( + text: String, + hidden: LiveHiddenUsers, + ) = search.findNotesStartingWith(text, hidden) + + override fun findUsersStartingWith( + prefix: String, + limit: Int, + ) = search.findUsersStartingWith(prefix, null, limit) + + override fun findPublicChatChannelsStartingWith(text: String) = search.findPublicChatChannelsStartingWith(text) + + override fun findEphemeralChatChannelsStartingWith(text: String) = search.findEphemeralChatChannelsStartingWith(text) + + override fun findLiveActivityChannelsStartingWith(text: String) = search.findLiveActivityChannelsStartingWith(text) + + fun Filter.match(note: Note): Boolean { + val event = note.event + return if (event != null) { + match(event) + } else { + false + } + } + + fun filter(filter: Filter): SortedSet = filter(filter) { true } + + /** + * Every note matching [filter]'s NIP-01 fields that also satisfies [predicate]. + * + * [predicate] is where anything the wire type cannot express belongs — a NIP-50 `search` the + * matcher does not read, and viewer policy like the mute list, which a relay has no knowledge + * of and a `Filter` therefore has no field for. Keeping it a separate parameter is what lets + * search reuse this path instead of hand-rolling its own scan. + */ + fun filter( + filter: Filter, + predicate: (Note) -> Boolean, + ): SortedSet { + val byKinds = filter.kinds?.filter { it.isAddressable() || it.isReplaceable() } + + val addressableMatches = + if (!byKinds.isNullOrEmpty()) { + val byAuthors = filter.authors + if (!byAuthors.isNullOrEmpty()) { + // optimized + byKinds.flatMap { kind -> + byAuthors.flatMap { pubkey -> + addressables.filter(kind, pubkey) { _, note -> + filter.match(note) && predicate(note) + } + } + } + } else { + // optimized + byKinds.flatMap { kind -> + addressables.filter(kind) { _, note -> + filter.match(note) && predicate(note) + } + } + } + } else { + addressables.filter { _, note -> + filter.match(note) && predicate(note) + } + } + + val noteMatches = + notes.filter { _, note -> + val event = note.event + if (event != null && event.kind.isRegular()) { + filter.match(event) && predicate(note) + } else { + false + } + } + + val all = (addressableMatches + noteMatches).toSortedSet(CreatedAtIdHexComparator) + val limit = filter.limit ?: return all + + // Sorted first, then cut. Both halves arrive in hash-walk order, so taking before sorting + // dropped whichever matches the walk happened to reach last — the newest ones as often as + // not — and a query with 200 addressable matches never showed a single regular note. + if (all.size <= limit) return all + return all.asSequence().take(limit).toCollection(sortedSetOf(CreatedAtIdHexComparator)) + } + + fun observeNotes(filter: Filter): Flow> = + callbackFlow { + val newFilter = + NoteListMatchingFilter(filter, this@EventCache::filter) { + trySend(it) + } + + newFilter.init() + + observables.register(filter, newFilter) + + awaitClose { + observables.unregister(newFilter) + } + }.buffer(kotlinx.coroutines.channels.Channel.CONFLATED) + + fun observeEvents(filter: Filter): Flow> = + callbackFlow { + val cachedFilter = + EventListMatchingFilter(filter, this@EventCache::filter) { + trySend(it) + } + + cachedFilter.init() + + observables.register(filter, cachedFilter) + + awaitClose { + observables.unregister(cachedFilter) + } + }.buffer(kotlinx.coroutines.channels.Channel.CONFLATED) + + /** + * Emits each new event for which [predicate] returns true, one at a time, + * as it is inserted into the cache. Unlike [observeEvents], this does not + * accumulate a list — useful for per-event reactive pipelines like + * notifications. + * + * The predicate runs on every insertion, so keep it cheap. Callers with a + * Nostr [Filter] can pass `filter::match`; compose additional local checks + * (rolling windows, derived fields the Filter grammar can't express) with + * `&&`. + */ + fun observeNewEvents(predicate: (Event) -> Boolean): Flow = + callbackFlow { + val newFilter = + NewEventMatchingFilter(predicate) { + trySend(it) + } + + // Unindexed: predicate is opaque, the index can't narrow. + // Caller is delivered every event and runs the predicate. + observables.registerUnindexed(newFilter) + + awaitClose { + observables.unregister(newFilter) + } + } + + fun observeNewEvents(filter: Filter): Flow = + callbackFlow { + val newFilter = + NewEventMatchingFilter(filter::match) { + trySend(it) + } + + observables.register(filter, newFilter) + + awaitClose { + observables.unregister(newFilter) + } + } + + @Suppress("UNCHECKED_CAST") + fun observeLatestEvent(filter: Filter) = observeEvents(filter).map { it.firstOrNull() } + + fun observeLatestNote(filter: Filter) = observeNotes(filter).map { it.firstOrNull() } + + override fun checkGetOrCreateUser(key: String): User? = runCatching { getOrCreateUser(key) }.getOrNull() + + fun load(keys: List): List = keys.mapNotNull(::checkGetOrCreateUser) + + fun load(keys: Set): Set = keys.mapNotNullTo(mutableSetOf(), ::checkGetOrCreateUser) + + override fun getOrCreateUser(pubkey: HexKey): User { + require(isValidHex(key = pubkey)) { "$pubkey is not a valid hex" } + // Pass `this` as the UserContext — User now resolves each pinned + // addressable note (kind:10002 / 10050 / 10019) lazily on first + // read, instead of all-or-nothing at construction time. + return users.getOrCreate(pubkey) { User(it, userContext) } + } + + /** [UserContext] bridge to this cache's addressable lookup. */ + private val userContext = UserContext(::getOrCreateAddressableNoteInternal) + + override fun getUserIfExists(pubkey: String): User? { + if (pubkey.isEmpty()) return null + return users.get(pubkey) + } + + override fun countUsers(predicate: (String, User) -> Boolean): Int { + var count = 0 + users.forEach { key, user -> + if (predicate(key, user)) count++ + } + return count + } + + fun countContactLists(predicate: (ContactListEvent) -> Boolean): Int { + var count = 0 + addressables.filter(ContactListEvent.KIND).forEach { note -> + val event = note.event as? ContactListEvent + if (event != null && predicate(event)) count++ + } + return count + } + + fun getAddressableNoteIfExists(key: String): AddressableNote? = Address.parse(key)?.let { addressables.get(it) } + + fun getAddressableNoteIfExists(address: Address): AddressableNote? = addressables.get(address) + + override fun getNoteIfExists(hexKey: String): Note? = if (hexKey.length == 64) notes.get(hexKey) else Address.parse(hexKey)?.let { addressables.get(it) } + + fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId) + + fun getPublicChatChannelIfExists(key: String): PublicChatChannel? = publicChatChannels.get(key) + + fun getEphemeralChatChannelIfExists(key: RoomId): EphemeralChatChannel? = ephemeralChannels.get(key) + + fun getGeohashChannelIfExists(geohash: String): GeohashChatChannel? = geohashChannels.get(geohash) + + fun getRelayGroupChannelIfExists(key: GroupId): RelayGroupChannel? = relayGroupChannels.get(key) + + /** Every relay group we know of that is hosted on [relay] (its channel directory). */ + override fun getRelayGroupChannelsOnRelay(relay: NormalizedRelayUrl): List = relayGroupChannels.filter { key, _ -> key.relayUrl == relay } + + override fun allRelayGroupChannels(): List = relayGroupChannels.values().toList() + + /** + * The [RelayGroupChannel] a group-scoped content [note] belongs to, resolved the same way + * [attachToRelayGroupIfScoped] keyed it: the serving-relay key first (fast O(1)), then the single + * channel bearing this group id when the note has no usable provenance relay. Read-only — used by + * feed filters that need a note's channel without scanning every channel's timeline. + */ + fun getRelayGroupChannelForContent(note: Note): RelayGroupChannel? { + val groupId = note.event?.groupId() ?: return null + note.relays.firstNotNullOfOrNull { getRelayGroupChannelIfExists(GroupId(groupId, it)) }?.let { return it } + return relayGroupChannels.filter { key, _ -> key.id == groupId }.singleOrNull() + } + + /** + * Every host relay of the NIP-29 group [event] is scoped to (its `h` tag), or an empty set when the + * event carries no group scope or the group is unknown to this cache. + * + * Keyed by group id alone rather than by [GroupId]: an event about to be *sent* (a reaction, a zap + * request, a comment) knows which room it belongs to but not which relay hosts it — that is exactly + * what this resolves. Group ids are relay-minted UUIDs, so the same id on two hosts is a + * theoretical case, and answering with both is the safe reading of it: the content reaches every + * host that claims the room, and none that don't. + */ + fun relayGroupHostsFor(event: Event): Set { + val groupId = event.groupId() ?: return emptySet() + return relayGroupChannels.filter { key, _ -> key.id == groupId }.mapTo(mutableSetOf()) { it.groupId.relayUrl } + } + + fun getLiveActivityChannelIfExists(key: Address): LiveActivitiesChannel? = liveChatChannels.get(key) + + fun getNoteIfExists(event: Event): Note? = + if (event is AddressableEvent) { + getAddressableNoteIfExists(event.addressTag()) + } else { + getNoteIfExists(event.id) + } + + fun getOrCreateNote(event: Event): Note = + if (event is AddressableEvent) { + getOrCreateAddressableNote(event.address()) + } else { + getOrCreateNote(event.id) + } + + fun checkGetOrCreateNote(etag: ETag): Note? { + if (isValidHex(etag.eventId)) { + return getOrCreateNote(etag) + } + return null + } + + override fun checkGetOrCreateNote(hexKey: String): Note? { + if (ATag.isATag(hexKey)) { + return checkGetOrCreateAddressableNote(hexKey) + } + if (isValidHex(hexKey)) { + val note = getOrCreateNote(hexKey) + val noteEvent = note.event + return if (noteEvent is AddressableEvent) { + // upgrade to the latest + val newNote = getOrCreateAddressableNote(noteEvent.address()) + + if (newNote.event == null) { + val author = note.author ?: getOrCreateUser(noteEvent.pubKey) + newNote.loadEvent(noteEvent as Event, author, emptyList()) + note.moveAllReferencesTo(newNote) + } + + newNote + } else { + note + } + } + return null + } + + override fun getEventStream(): com.vitorpamplona.amethyst.commons.model.cache.ICacheEventStream = + object : com.vitorpamplona.amethyst.commons.model.cache.ICacheEventStream { + override val newEventBundles = live.newEventBundles + override val deletedEventBundles = live.deletedEventBundles + } + + override fun hasBeenDeleted(event: Any): Boolean = + if (event is Event) { + deletionIndex.hasBeenDeleted(event) + } else { + false + } + + /** + * Checks if a kind-5 event from the addressable's own author has deleted this + * address. Works for empty addressable shells whose event is not loaded yet. + */ + fun hasBeenDeleted(address: Address): Boolean = deletionIndex.hasBeenDeleted(address, address.pubKeyHex) + + fun getOrAddAliasNote( + idHex: String, + note: Note, + ): Note { + require(isValidHex(idHex)) { "$idHex is not a valid hex" } + + return notes.getOrCreate(idHex) { + note + } + } + + override fun getOrCreateNote(hex: String): Note { + require(isValidHex(hex)) { "$hex is not a valid hex" } + + return notes.getOrCreate(hex) { + Note(hex) + } + } + + fun getOrCreateChatroomList(key: HexKey): ChatroomList = chatroomList.getOrCreate(key) { ChatroomList(key) } + + fun getOrCreatePublicChatChannel(key: HexKey): PublicChatChannel = publicChatChannels.getOrCreate(key) { PublicChatChannel(key) } + + fun getOrCreateLiveChannel(key: Address): LiveActivitiesChannel = liveChatChannels.getOrCreate(key) { LiveActivitiesChannel(key) } + + fun getOrCreateEphemeralChannel(key: RoomId): EphemeralChatChannel = ephemeralChannels.getOrCreate(key) { EphemeralChatChannel(key) } + + fun getOrCreateGeohashChannel(geohash: String): GeohashChatChannel = geohashChannels.getOrCreate(geohash) { GeohashChatChannel(geohash) } + + fun getOrCreateRelayGroupChannel(key: GroupId): RelayGroupChannel = relayGroupChannels.getOrCreate(key) { RelayGroupChannel(key) } + + fun getConcordChannelIfExists(key: ConcordChannelId): ConcordChannel? = concordChannels.get(key) + + fun getOrCreateConcordChannel(key: ConcordChannelId): ConcordChannel = concordChannels.getOrCreate(key) { ConcordChannel(key) } + + /** + * Any known channel of Concord community [communityId], or null when we hold none. + * + * A community is not itself a cached object — it has no metadata event, only a folded Control + * Plane — so its display fields (`communityName`, icon, relays) are carried on every one of its + * channels. Callers that need to *name* a community therefore ask for whichever channel we have. + */ + fun getAnyConcordChannelOfCommunity(communityId: HexKey): ConcordChannel? = concordChannels.filter { key, _ -> key.communityId == communityId }.firstOrNull() + + /** + * Lands a decrypted Concord chat rumor in the cache as a real Note and, for + * message-like kinds, attaches it to its channel so the shared chat feed and + * the Messages inbox render it (with previews, threading, OTS, reactions/zaps + * reusing the same id-keyed machinery as every other chat). Reactions (kind 7), + * deletes (kind 5), etc. are consumed too — they wire to their target Note by + * `e`-tag through [justConsume] — but are not themselves added as channel rows. + * + * Fed by [com.vitorpamplona.amethyst.commons.model.concord.ConcordSessionManager] + * once a wrap decrypts + validates against the folded Control Plane. + */ + fun consumeConcordRumor( + communityId: String, + channelIdHex: String, + rumor: Event, + seenOnRelays: Set = emptySet(), + ) { + // Attach to the channel BEFORE justConsume sets the event and notifies feeds, + // so the note already carries its ConcordChannel gatherer when it flows through + // the Messages-list incremental filter (which routes rows by that gatherer). + val messageRow = + if (rumor is ChatEvent || rumor is CommentEvent) { + val ch = getOrCreateConcordChannel(ConcordChannelId(communityId, channelIdHex)) + val note = getOrCreateNote(rumor.id) + // Skip attaching a row for a message we already know is deleted (its kind-5 delete + // was processed first). Otherwise every reproject — which re-emits the whole wrap + // buffer — would re-add then re-remove it, churning the feed. justConsume still + // records the (already-known) deletion below; a delete arriving LATER is handled by + // the normal deletion cascade unlinking the note from its gatherers. + if (!deletionIndex.hasBeenDeleted(rumor)) ch.addNote(note) + ch to note + } else { + null + } + // wasVerified = true: a Concord rumor is unsigned (its `sig` is empty), so a signature + // check would fail and the event would never load onto its Note — leaving the chat row + // stuck on the "loading / not found" placeholder. Its authenticity is already established + // by the envelope open path (ConcordStreamEnvelope.open verifies the seal signature, + // binds rumor.pubKey == seal.pubKey, and checks rumor.verifyId()), exactly like a NIP-59 + // gift-wrapped DM rumor, so we consume it as pre-verified. + justConsume(rumor, null, true) + + // A Concord rumor is decrypted locally with the plane key, so it arrives with no + // per-relay attribution (relay = null above). Stamp the relays its carrying wrap was + // seen on — mirroring NIP-17's addRelayToNoteAndInners — so the chat UI can show where + // the message actually came from, not just the channel's configured relays. + if (seenOnRelays.isNotEmpty()) { + getNoteIfExists(rumor.id)?.takeIf { it.event != null }?.let { note -> + seenOnRelays.forEach { note.addRelay(it) } + } + } + + // justConsume bails without loading the event when the rumor has already been deleted + // (a kind-5 delete referencing it was processed first — easy to hit in Concord because a + // reproject re-emits the whole wrap buffer and ordering isn't guaranteed) or fails to + // verify. We attached the row up front, so an unpopulated note would otherwise linger as a + // permanent "Event is loading…" ghost. Drop it; the reverse order (delete after the message) + // is already handled by the normal deletion cascade unlinking the note from its gatherers. + messageRow?.let { (ch, note) -> + if (note.event == null) { + ch.removeNote(note) + } else { + // The row was attached (addNote) BEFORE justConsume set the event, so addNote saw a + // null createdAt and could not pick lastNote or order the feed. The event is loaded + // now — refresh so the channel's last-message preview, unread count, and ordering are + // correct (otherwise lastNote stays null forever and every row reads "No messages yet"). + ch.refreshAfterEventLoad(note) + } + } + } + + fun checkGetOrCreatePublicChatChannel(key: String): PublicChatChannel? { + if (isValidHex(key)) { + return getOrCreatePublicChatChannel(key) + } + return null + } + + private fun isValidHex(key: String): Boolean { + if (key.length != 64) return false + return Hex.isHex64(key) + } + + override fun checkGetOrCreateAddressableNote(key: String): AddressableNote? = + try { + val addr = Address.parse(key) + if (addr != null) { + getOrCreateAddressableNote(addr) + } else { + null + } + } catch (e: IllegalArgumentException) { + Log.e("LocalCache", "Invalid Key to create channel: $key", e) + null + } + + fun getOrCreateAddressableNoteInternal(key: Address): AddressableNote = addressables.getOrCreate(key) { AddressableNote(key) } + + override fun getOrCreateAddressableNote(address: Address): AddressableNote { + val note = getOrCreateAddressableNoteInternal(address) + // Loads the user outside a Syncronized block to avoid blocking + if (note.author == null) { + note.author = checkGetOrCreateUser(address.pubKeyHex) + } + return note + } + + fun getOrCreateNote(key: GenericETag): Note { + val note = getOrCreateNote(key.eventId) + // Loads the user outside a Syncronized block to avoid blocking + val possibleAuthor = key.author + if (note.author == null && possibleAuthor != null) { + note.author = checkGetOrCreateUser(possibleAuthor) + } + val relayHint = key.relay + if (relayHint != null) { + note.addRelay(relayHint) + } + return note + } + + fun consume( + event: MetadataEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + // new event + consumeBaseReplaceable(event, relay, wasVerified) + + val user = getOrCreateUser(event.pubKey) + + if (user.metadata().shouldUpdateWith(event)) { + val newUserMetadata = event.contactMetaData() + if (newUserMetadata != null && (wasVerified || justVerify(event))) { + user.updateUserInfo(newUserMetadata, event) + if (relay != null) { + user.addRelayBeingUsed(relay, event.createdAt) + } + + return true + } + } + + return false + } + + fun consumeRegularEvent( + event: Event, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + // A gift wrap re-delivered by another relay is a duplicate (returns + // false below and is never re-processed), so drill into the already + // unwrapped chain here — otherwise the relay never reaches the + // rumor note that the chat UI actually renders. + addRelayToNoteAndInners(note, relay) + } + + // Already processed this event. + if (note.event != null) return false + + if (event is BaseNoteEvent && antiSpam.isSpam(event, relay)) { + return false + } + + return if (wasVerified || justVerify(event)) { + val replyTo = computeReplyTo(event) + + note.loadEvent(event, author, replyTo) + + // Counts the replies + replyTo.forEach { it.addReply(note) } + + // NIP-18 quote reposts: a note carrying a `q` tag is a quote-repost of the + // quoted note. Count it as a boost so it shows in the quoted note's repost + // counter alongside kind:6/kind:16 reposts. The quoted note is deliberately + // kept out of `replyTo` so the quote still renders as a root post in the home + // feed (see Note.isNewThread); deletion cleanup lives in CachePruner.unlinkAndRemove. + addQuoteBoosts(event, note, replyTo) + + refreshNewNoteObservers(note) + + true + } else { + false + } + } + + /** + * Adds [note] as a boost of every event/address referenced by a NIP-18 `q` tag + * (a quote-repost). Targets already in [replyTo] are skipped so a note that both + * replies to and quotes the same note isn't counted twice, and self-quotes are + * ignored. + */ + private fun addQuoteBoosts( + event: Event, + note: Note, + replyTo: List, + ) { + event.taggedQuoteIds().forEach { quotedId -> + val quoted = checkGetOrCreateNote(quotedId) + if (quoted != null && quoted != note && quoted !in replyTo) { + quoted.addBoost(note) + } + } + } + + fun consume( + event: NipTextEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val version = getOrCreateNote(event.id) + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + val isVerified = + if (version.event == null && (wasVerified || justVerify(event))) { + version.loadEvent(event, author, emptyList()) + version.moveAllReferencesTo(note) + true + } else { + wasVerified + } + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event?.id == event.id) return wasVerified + + if (antiSpam.isSpam(event, relay)) { + return false + } + + if (isVerified || justVerify(event)) { + val replyTo = computeReplyTo(event) + + if (event.createdAt > (note.createdAt() ?: 0L)) { + note.loadEvent(event, author, replyTo) + + refreshNewNoteObservers(note) + + return true + } + } + + return false + } + + fun consume( + event: LongTextNoteEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val version = getOrCreateNote(event.id) + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + val isVerified = + if (version.event == null && (wasVerified || justVerify(event))) { + version.loadEvent(event, author, emptyList()) + version.moveAllReferencesTo(note) + true + } else { + wasVerified + } + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event?.id == event.id) return wasVerified + + if (antiSpam.isSpam(event, relay)) { + return false + } + + if (isVerified || justVerify(event)) { + val replyTo = computeReplyTo(event) + + if (event.createdAt > (note.createdAt() ?: 0L)) { + note.loadEvent(event, author, replyTo) + + refreshNewNoteObservers(note) + + return true + } + } + + return false + } + + fun consume( + event: PaymentTargetsEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val version = getOrCreateNote(event.id) + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + val isVerified = + if (version.event == null && (wasVerified || justVerify(event))) { + version.loadEvent(event, author, emptyList()) + version.moveAllReferencesTo(note) + true + } else { + wasVerified + } + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event?.id == event.id) return wasVerified + + if (antiSpam.isSpam(event, relay)) { + return false + } + + if ((isVerified || justVerify(event)) && event.createdAt > (note.createdAt() ?: 0L)) { + val replyTo = computeReplyTo(event) + + note.loadEvent(event, author, replyTo) + + refreshNewNoteObservers(note) + + return true + } + + return false + } + + fun consume( + event: WikiNoteEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val version = getOrCreateNote(event.id) + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + val isVerified = + if (version.event == null && (wasVerified || justVerify(event))) { + version.loadEvent(event, author, emptyList()) + version.moveAllReferencesTo(note) + true + } else { + wasVerified + } + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event?.id == event.id) return wasVerified + + if (antiSpam.isSpam(event, relay)) { + return false + } + + if ((isVerified || justVerify(event)) && event.createdAt > (note.createdAt() ?: 0L)) { + val replyTo = computeReplyTo(event) + + note.loadEvent(event, author, replyTo) + + refreshNewNoteObservers(note) + + return true + } + + return false + } + + @Suppress("DEPRECATION") + fun computeReplyTo(event: Event): List = + when (event) { + is ZapPollEvent -> { + event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } + } + + is LongTextNoteEvent -> { + event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } + } + + is GitReplyEvent -> { + event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.mapNotNull { checkGetOrCreateNote(it) } + } + + is GitPullRequestUpdateEvent -> { + // Link the update to its parent PR so it lands in the PR's + // replies collection (and picks up its target for threading). + // The repository ATag isn't a reply target — skip it. + listOfNotNull(event.parentPullRequestId()?.let { checkGetOrCreateNote(it) }) + } + + is GitStatusEvent -> { + // A status event roots itself at a patch/PR/issue via a + // marked-`root` `e` tag; link only that so the transition + // appears in the target's replies (GitStatusIndex reduces the + // observed stream separately and doesn't need this wiring, but + // ThreadFeedView and the notifications-tab reply chain do). + listOfNotNull(event.rootEventId()?.let { checkGetOrCreateNote(it) }) + } + + is TextNoteEvent -> { + event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } + } + + is CommentEvent -> { + event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } + } + + is StreamMessageV2Event -> { + // A Buzz thread reply links to the message it answers (its `reply`-marked e-tag) so it + // lands in that message's replies (the minichat). A plain message / non-reply has no marker. + listOfNotNull(event.tags.buzzThreadReply()?.let { checkGetOrCreateNote(it) }) + } + + is VoiceReplyEvent -> { + event.markedReplyTos().mapNotNull { checkGetOrCreateNote(it) } + } + + is ChatMessageEvent -> { + event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } + } + + is ChatMessageEncryptedFileHeaderEvent -> { + event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } + } + + is LnZapEvent -> { + event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + + event.taggedAddresses().map { getOrCreateAddressableNote(it) } + + (event.zapRequest?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptyList()) + } + + is OnchainZapEvent -> { + // NIP-BC zaps can target an event id (e), an addressable event (a), + // or just the recipient profile (p). Profile-only zaps have no + // Note target and are surfaced through profile zap queries. + buildList { + event.zappedEvent()?.let { checkGetOrCreateNote(it)?.let { add(it) } } + event.zappedAddress()?.let { coord -> + Address.parse(coord)?.let { add(getOrCreateAddressableNote(it)) } + } + } + } + + is Bolt12ZapEvent -> { + // NIP-B1 BOLT12 zaps target an event (e), an addressable event (a), + // or just the recipient profile (p) — same shape as onchain zaps. + buildList { + event.zappedEvent()?.let { checkGetOrCreateNote(it)?.let { add(it) } } + event.zappedAddress()?.let { coord -> + Address.parse(coord)?.let { add(getOrCreateAddressableNote(it)) } + } + } + } + + is NutzapEvent -> { + // The zapped event is carried in the kind:9321's `e` tags + // (and optionally an `a` tag for addressables). Whichever + // notes those resolve to receive the nutzap entry — + // analogous to how LnZapEvent flows into `addZap`. + event.linkedEventIds().mapNotNull { checkGetOrCreateNote(it) } + } + + is LnZapRequestEvent -> { + event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + + event.taggedAddresses().map { getOrCreateAddressableNote(it) } + } + + is AcceptedBadgeSetEvent, is ProfileBadgesEvent -> { + event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } + + event.taggedAddresses().map { getOrCreateAddressableNote(it) } + } + + is BadgeAwardEvent -> { + event.awardDefinition().map { getOrCreateAddressableNote(it) } + } + + is PrivateDmEvent -> { + event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } + } + + is RepostEvent, is GenericRepostEvent -> { + val repost = event as BaseRepostEvent + listOfNotNull( + repost.boostedEventId()?.let { checkGetOrCreateNote(it) }, + repost.boostedAddress()?.let { getOrCreateAddressableNote(it) }, + ) + } + + is CommunityPostApprovalEvent -> { + event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) } + + event.approvedAddresses().map { getOrCreateAddressableNote(it) } + } + + is ReactionEvent -> { + event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + + event.taggedAddresses().map { getOrCreateAddressableNote(it) } + } + + is WakeUpEvent -> { + // Link the referenced events so filterMissingEvents will query + // for them when the WakeUp note is in an EventFinder subscription. + event.eventIds().mapNotNull { checkGetOrCreateNote(it) } + } + + is ChannelMessageEvent -> { + event.tagsWithoutCitations().filter { it != event.channelId() }.mapNotNull { checkGetOrCreateNote(it) } + } + + is LiveActivitiesChatMessageEvent -> { + event.tagsWithoutCitations().filter { it != event.activity()?.toTag() }.mapNotNull { checkGetOrCreateNote(it) } + } + + is TorrentCommentEvent -> { + event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } + } + + is ChatEvent -> { + // Amethyst's own kind:9 replies carry the parent as a NIP-18 + // `q` tag (see ChatEvent.replyingTo), but the broader Marmot + // ecosystem — WhiteNoise in particular — threads kind:9 chats + // with a plain NIP-10 `e` tag. Accept both so inbound replies + // from either client show their quote bubble in the feed. + val eTagTargets = + event.tags + .filter { it.size > 1 && it[0] == "e" } + .map { it[1] } + val qTagTargets = event.quotedEvents().map { it.eventId } + (eTagTargets + qTagTargets).mapNotNull { checkGetOrCreateNote(it) } + } + + else -> { + emptyList() + } + } + + private fun consume( + event: LiveActivitiesEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val version = getOrCreateNote(event.id) + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + val isVerified = + if (version.event == null && (wasVerified || justVerify(event))) { + version.loadEvent(event, author, emptyList()) + version.moveAllReferencesTo(note) + true + } else { + wasVerified + } + + if (note.event?.id == event.id) return false + + if (event.createdAt > (note.createdAt() ?: 0L) && (isVerified || justVerify(event))) { + note.loadEvent(event, author, emptyList()) + + val channel = getOrCreateLiveChannel(note.address) + + if (relay != null) { + channel.addRelay(relay) + } + + val creator = event.host()?.let { checkGetOrCreateUser(it.pubKey) } ?: author + + channel.updateChannelInfo(creator, event, note) + + refreshNewNoteObservers(note) + + return true + } + + return false + } + + fun consume( + event: StatusEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val author = getOrCreateUser(event.pubKey) + val note = event.toAddressableNote() + val new = consumeBaseReplaceable(event, relay, wasVerified) + + if (new) { + author.statusState().addStatus(note) + } + + return new + } + + fun Event.toNote() = getOrCreateNote(id) + + fun AddressableEvent.toAddressableNote() = getOrCreateAddressableNote(address()) + + fun consume( + event: ContactCardEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val note = event.toAddressableNote() + val new = consumeBaseReplaceable(event, relay, wasVerified) + + if (new) { + val about = checkGetOrCreateUser(event.aboutUser()) ?: return new + about.cards().addCard(note) + } + + return new + } + + fun consume( + event: OtsEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val version = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + // Already processed this event. + if (version.event != null) return false + + if (wasVerified || justVerify(event)) { + version.loadEvent(event, author, emptyList()) + + // Anchor the attestation to the note it timestamps (like an edit to its message), so it + // survives exactly as long as that note and is dropped when the note is deleted or pruned. + // addTimestamp invalidates the target's `ots` flow so the OTS pill re-derives — the old + // code invalidated the attestation's OWN (observer-less) flow, so the target never updated. + event.digestEventId()?.let { targetId -> + getOrCreateNote(targetId).addTimestamp(version) + } + + refreshNewNoteObservers(version) + return true + } + + return false + } + + private fun consumeBaseReplaceable( + event: Event, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + // TODO: Redo the Event sctructure in Quartz to avoid this check + check(event is AddressableEvent) { "Event must be addressable: ${event.kind}" } + + val version = getOrCreateNote(event.id) + val replaceableNote = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + val isVerified = + if (version.event == null && (wasVerified || justVerify(event))) { + version.loadEvent(event, author, emptyList()) + version.moveAllReferencesTo(replaceableNote) + true + } else { + wasVerified + } + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + replaceableNote.addRelay(relay) + } + + // Already processed this event. + if (replaceableNote.event?.id == event.id) return isVerified + + return if (event.createdAt > (replaceableNote.createdAt() ?: 0L) && (isVerified || justVerify(event))) { + // clear index from previous tags + replaceableNote.replyTo?.forEach { + it.removeNote(replaceableNote) + } + + replaceableNote.loadEvent(event, author, computeReplyTo(event)) + + refreshNewNoteObservers(replaceableNote) + + true + } else { + false + } + } + + fun consume( + event: DeletionEvent, + 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 + + return 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() ?: 0L) <= 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) + } + } + + 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() ?: 0L) <= event.createdAt) { + // Counts the replies + deleteNote(deleteNote) + + addressables.remove(deleteNote.address) + } + } + + notes.forEach { _, note -> + val noteEvent = note.event + if (noteEvent is AddressableEvent && + noteEvent.addressTag() in addressSet && + noteEvent.pubKey == event.pubKey && + noteEvent.createdAt <= event.createdAt + ) { + deleteNote(note) + } + } + } + + refreshNewNoteObservers(note) + + true + } else { + false + } + } + + override 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) } + is GeohashChatEvent -> noteEvent.geohash()?.let { getGeohashChannelIfExists(it) } + else -> null + } + + /** + * NIP-09 delete of a single targeted event. + * + * Removal has two halves: unlinking the note from everything that points AT it + * (its parents, channels, and the per-user report/card/status/poll indexes — + * all handled by [CachePruner.unlinkAndRemove]); and dealing with the note's OWN children + * (the notes that point at IT). The delete path and the prune path share the + * first half and differ only on the second: + * - delete (here): the children are independent events and stay in the cache; + * [Note.detachFromChildren] only severs their back-reference so the removed + * shell can neither leak (held alive by a child's `replyTo`) nor be later + * resurrected by `computeReplyTo` as a second Note for the same id. + * - prune (see [CachePruner.unlinkAndRemove] callers): the whole child subtree is removed. + * + * Rumors additionally drop the envelope notes that delivered them. + */ + private fun deleteNote(deleteNote: Note) { + deleteEnvelopes(deleteNote) + + deleteNote.detachFromChildren() + + pruner.unlinkAndRemove(deleteNote) + } + + /** + * Removes the envelope notes that delivered [rumorNote]'s rumor: its + * host (normally the kind-1059 wrap; a bare kind-13 seal otherwise) + * and, when the host is a wrap, the seal layer it carried. Public + * events have no envelopes and are ignored. + */ + fun deleteEnvelopes(rumorNote: Note) { + val host = rumorNote.rumorHost ?: return + + getNoteIfExists(host.id)?.let { hostNote -> + (hostNote.event as? GiftWrapEvent)?.innerEventId?.let { sealId -> + getNoteIfExists(sealId)?.let { sealNote -> + sealNote.clearFlow() + refreshDeletedNoteObservers(sealNote) + } + notes.remove(sealId) + } + hostNote.clearFlow() + refreshDeletedNoteObservers(hostNote) + } + + notes.remove(host.id) + rumorNote.rumorHost = null + } + + fun consume( + event: RepostEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val note = getOrCreateNote(event.id) + + if (relay != null) { + getOrCreateUser(event.pubKey).addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return false + + if (wasVerified || justVerify(event)) { + val author = getOrCreateUser(event.pubKey) + val repliesTo = computeReplyTo(event) + + note.loadEvent(event, author, repliesTo) + + // Counts the replies + repliesTo.forEach { it.addBoost(note) } + + event.containedPost()?.let { + checkDeletionAndConsume(it, relay, false) + } + + refreshNewNoteObservers(note) + + return true + } + return false + } + + fun consume( + event: GenericRepostEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val note = getOrCreateNote(event.id) + + if (relay != null) { + getOrCreateUser(event.pubKey).addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return false + + if (wasVerified || justVerify(event)) { + val author = getOrCreateUser(event.pubKey) + val repliesTo = computeReplyTo(event) + + note.loadEvent(event, author, repliesTo) + + // Counts the replies + repliesTo.forEach { it.addBoost(note) } + + event.containedPost()?.let { + checkDeletionAndConsume(it, relay, false) + } + + refreshNewNoteObservers(note) + + return true + } + + return false + } + + fun consume( + event: CommunityPostApprovalEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val note = getOrCreateNote(event.id) + + // Approval notes render their own relay list directly in community feeds + // (there is no repost-style indirection to a replyTo for them), so without + // this attribution the "accepted by relays" gallery line stays empty forever. + if (relay != null) { + getOrCreateUser(event.pubKey).addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return false + + if (wasVerified || justVerify(event)) { + val author = getOrCreateUser(event.pubKey) + + val communities = event.communityAddresses() + val eventsApproved = computeReplyTo(event) + + val repliesTo = communities.map { getOrCreateAddressableNote(it) } + + note.loadEvent(event, author, eventsApproved) + + // Counts the replies + repliesTo.forEach { it.addBoost(note) } + + eventsApproved.forEach { it.addBoost(note) } + + event.containedPost()?.let { + checkDeletionAndConsume(it, relay, false) + } + + refreshNewNoteObservers(note) + + return true + } + + return false + } + + fun consume( + event: ReactionEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val note = getOrCreateNote(event.id) + + // Already processed this event. + if (note.event != null) return true + + if (wasVerified || justVerify(event)) { + val author = getOrCreateUser(event.pubKey) + val repliesTo = computeReplyTo(event) + + note.loadEvent(event, author, repliesTo) + + repliesTo.forEach { it.addReaction(note) } + + refreshNewNoteObservers(note) + + return true + } + + return false + } + + fun consume( + event: LabelEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val note = getOrCreateNote(event.id) + + // Already processed this event. + if (note.event != null) return true + + if (wasVerified || justVerify(event)) { + val author = getOrCreateUser(event.pubKey) + val repliesTo = computeReplyTo(event) + + note.loadEvent(event, author, repliesTo) + + // Attach NIP-32 hashtag labels (namespace #t) to the labeled events so the + // hashtag feed can surface posts tagged by a follow and attribute the labeler. + val hashtags = event.hashtagAssociations() + if (hashtags.isNotEmpty()) { + event.labeledEvents().mapNotNull { checkGetOrCreateNote(it) }.forEach { target -> + hashtags.forEach { hashtag -> target.addLabel(hashtag, note) } + + // If the labeled post is already in cache, re-notify feed observers so the + // hashtag feed can pick it up now that a (possibly followed) user labeled it. + if (target.event != null) refreshNewNoteObservers(target) + } + } + + refreshNewNoteObservers(note) + + return true + } + + return false + } + + fun consume( + event: ReportEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val note = getOrCreateNote(event.id) + + val new = consumeRegularEvent(event, relay, wasVerified) + + if (new) { + val authorsReported = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.pubKey) } + val eventsReported = + event.reportedPost().mapNotNull { checkGetOrCreateNote(it.eventId) } + + event.reportedAddresses().map { getOrCreateAddressableNote(it.address) } + + // Every report that names an author also lands in the naming index, including those + // filed against a note rather than a profile. Read only by the DM sender warning — the + // hide threshold keeps counting `receivedReportsByAuthor` alone. + if (eventsReported.isEmpty()) { + authorsReported.forEach { author -> + val reports = author.reports() + reports.addReport(note) + reports.addReportNamingUser(note) + } + } else { + eventsReported.forEach { it.addReport(note) } + + // Index only the authors whose own `p` tag carries a report type: an event-scoped + // report can `p`-tag an incidentally-mentioned third party with no type of its own, + // and there is no threshold here to absorb that noise the way + // `receivedReportsByAuthor`'s hide path does. + val explicitlyTyped = event.reportedAuthorsWithOwnType().mapTo(mutableSetOf()) { it.pubKey } + authorsReported.forEach { author -> + if (author.pubkeyHex in explicitlyTyped) author.reports().addReportNamingUser(note) + } + } + } + + return new + } + + fun consume( + event: ChannelCreateEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + // Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") + val oldChannel = getOrCreatePublicChatChannel(event.id) + val author = getOrCreateUser(event.pubKey) + val note = getOrCreateNote(event.id) + + val isVerified = + if (note.event == null && (wasVerified || justVerify(event))) { + oldChannel.addNote(note, relay) + note.loadEvent(event, author, emptyList()) + + refreshNewNoteObservers(note) + true + } else { + wasVerified + } + + if (event.createdAt <= oldChannel.updatedMetadataAt) { + return false // older data, does nothing + } + + if ((oldChannel.creator == null || oldChannel.creator == author) && (isVerified || justVerify(event))) { + oldChannel.updateChannelInfo(author, event, note) + } + + return isVerified + } + + fun consume( + event: ChannelMetadataEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val channelId = event.channelId() + if (channelId.isNullOrBlank()) return false + + // new event + val oldChannel = checkGetOrCreatePublicChatChannel(channelId) ?: return false + + val author = getOrCreateUser(event.pubKey) + val note = getOrCreateNote(event.id) + + val isVerified = + if (event.createdAt > oldChannel.updatedMetadataAt) { + if (wasVerified || justVerify(event)) { + oldChannel.updateChannelInfo(author, event, note) + true + } else { + false + } + } else { + wasVerified + } + + if (note.event == null && (isVerified || justVerify(event))) { + oldChannel.addNote(note, relay) + note.loadEvent(event, author, emptyList()) + + refreshNewNoteObservers(note) + } + + return isVerified + } + + fun consume( + event: ChannelMessageEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val channelId = event.channelId() ?: return false + + val new = consumeRegularEvent(event, relay, wasVerified) + + 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) + } + + return new + } + + fun consume( + event: EphemeralChatEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val roomId = event.roomId() ?: return false + + val new = consumeRegularEvent(event, relay, wasVerified) + + if (new) { + val note = getOrCreateNote(event.id) + val channel = getOrCreateEphemeralChannel(roomId) + channel.addNote(note, relay) + } + + return new + } + + /** + * Public geohash chat message (kind 20000). Routes into the cell's + * [GeohashChatChannel]. Presence (kind 20001) is deliberately NOT consumed + * here — it is an empty-content heartbeat handled by the live chat screen, so + * it never becomes a room's "last message". + */ + fun consume( + event: GeohashChatEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val geohash = event.geohash() ?: return false + + val new = consumeRegularEvent(event, relay, wasVerified) + + if (new) { + val note = getOrCreateNote(event.id) + val channel = getOrCreateGeohashChannel(geohash) + channel.addNote(note, relay) + } + + return new + } + + /** + * NIP-29 addressables (kinds 39000-39005) are authoritative for a group's metadata, + * roster, roles and pins ONLY when signed by the relay's own key — the NIP-11 `self` + * pubkey. This returns false only when we can positively tell an event is NOT relay-signed + * (the relay advertises a `self` and the event's author differs), so a stray or malicious + * user-published 39000/39001/… served by a lax relay can't overwrite a group's state (e.g. + * inject itself into the admin list). When `self` isn't known yet — the NIP-11 doc hasn't + * loaded, or the relay doesn't advertise one — we don't block, so legitimate groups still + * populate and this never regresses a relay whose key we simply haven't fetched. + */ + private fun isRelaySignedGroupEvent( + event: Event, + relay: NormalizedRelayUrl, + ): Boolean { + val self = appHost.relaySelfPubKey(relay) ?: return true + return event.pubKey == self + } + + /** + * NIP-29 relay-signed group metadata (kind 39000). Stored as an addressable + * note and used to populate the [RelayGroupChannel]'s name/picture/about/ + * flags. The group is keyed by (host relay + group id): unlike NIP-C7, a + * NIP-29 event does not carry its host relay in a tag — the host is the relay + * that served it — so the channel can only be associated when we know the + * serving relay (provenance). With no relay we still store the metadata. + */ + fun consume( + event: GroupMetadataEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val new = consumeBaseReplaceable(event, relay, wasVerified) + + if (relay != null && isRelaySignedGroupEvent(event, relay)) { + val note = getOrCreateAddressableNote(event.address()) + val channel = getOrCreateRelayGroupChannel(GroupId(event.groupId(), relay)) + (note.event as? GroupMetadataEvent)?.let { channel.updateGroupInfo(it, note) } + } + + return new + } + + /** NIP-29 relay-signed member list (kind 39002) → the group's roster. */ + fun consume( + event: GroupMembersEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val new = consumeBaseReplaceable(event, relay, wasVerified) + if (relay != null && isRelaySignedGroupEvent(event, relay)) { + val latest = getOrCreateAddressableNote(event.address()).event as? GroupMembersEvent + latest?.let { getOrCreateRelayGroupChannel(GroupId(it.groupId(), relay)).updateMembers(it) } + } + return new + } + + /** NIP-29 relay-signed admin list (kind 39001) → the group's roster. */ + fun consume( + event: GroupAdminsEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val new = consumeBaseReplaceable(event, relay, wasVerified) + if (relay != null && isRelaySignedGroupEvent(event, relay)) { + val latest = getOrCreateAddressableNote(event.address()).event as? GroupAdminsEvent + latest?.let { getOrCreateRelayGroupChannel(GroupId(it.groupId(), relay)).updateAdmins(it) } + } + return new + } + + /** NIP-29 relay-signed pinned-message list (kind 39005) → the group's pins. */ + fun consume( + event: GroupPinnedEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val new = consumeBaseReplaceable(event, relay, wasVerified) + if (relay != null && isRelaySignedGroupEvent(event, relay)) { + val latest = getOrCreateAddressableNote(event.address()).event as? GroupPinnedEvent + latest?.let { getOrCreateRelayGroupChannel(GroupId(it.groupId(), relay)).updatePinned(it) } + } + return new + } + + /** NIP-29 relay-declared supported roles (kind 39003) → the group's role set. */ + fun consume( + event: SupportedRolesEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val new = consumeBaseReplaceable(event, relay, wasVerified) + if (relay != null && isRelaySignedGroupEvent(event, relay)) { + val latest = getOrCreateAddressableNote(event.address()).event as? SupportedRolesEvent + latest?.let { getOrCreateRelayGroupChannel(GroupId(it.groupId(), relay)).updateSupportedRoles(it) } + } + return new + } + + /** + * Buzz/NIP-43 community (relay-wide) membership snapshot (kind 13534) → the relay's community roster. + * Buzz grants participation across ALL its channels off this list, not the per-channel 39002, so + * feeding it into [BuzzCommunityMembership] lets [RelayGroupChannel.membershipOf] recognise a + * community member even when a channel roster omits them. Scoped to Buzz relays (the concept is + * Buzz-specific and the event is relay-signed + NIP-70 relay-only). + */ + fun consume( + event: RelayMembershipListEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val new = consumeBaseReplaceable(event, relay, wasVerified) + if (relay != null && BuzzRelayDialect.isBuzz(relay)) { + if (BuzzCommunityMembership.updateSnapshot(relay, event.members().toSet(), event.createdAt)) { + refreshRelayGroupMembership(relay) + } + } + return new + } + + /** Buzz/NIP-43 community member-added delta (kind 8000) → adds to the relay's [BuzzCommunityMembership]. */ + fun consume( + event: RelayAddMemberEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val new = consumeBuzzRegularEvent(event, relay, wasVerified) + if (relay != null && BuzzRelayDialect.isBuzz(relay)) { + if (BuzzCommunityMembership.applyDelta(relay, add = event.memberPubKeys().toSet(), createdAt = event.createdAt)) { + refreshRelayGroupMembership(relay) + } + } + return new + } + + /** Buzz/NIP-43 community member-removed delta (kind 8001) → removes from the relay's [BuzzCommunityMembership]. */ + fun consume( + event: RelayRemoveMemberEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val new = consumeBuzzRegularEvent(event, relay, wasVerified) + if (relay != null && BuzzRelayDialect.isBuzz(relay)) { + if (BuzzCommunityMembership.applyDelta(relay, remove = event.memberPubKeys().toSet(), createdAt = event.createdAt)) { + refreshRelayGroupMembership(relay) + } + } + return new + } + + /** + * Poke every relay-group channel on [relay] so the `membershipOf` community-level fallback re-renders + * (community membership lives outside the per-channel roster, so nothing else invalidates their state). + */ + private fun refreshRelayGroupMembership(relay: NormalizedRelayUrl) { + getRelayGroupChannelsOnRelay(relay).forEach { it.updateChannelInfo() } + } + + /** + * Marks the serving relay as Buzz, but only off a VERIFIED event: the mark changes + * what the composer sends (40002 vs kind 9) and how new channels on the relay are + * treated, so an unverifiable frame from a buggy/hostile relay must not flip it. + * The note-has-event check is the same verification gate the attach path uses. + */ + private fun markBuzzIfVerified( + event: Event, + relay: NormalizedRelayUrl?, + ) { + if (relay != null && getNoteIfExists(event.id)?.event != null) { + BuzzRelayDialect.mark(relay) + } + } + + /** + * Consume + channel-attach for Buzz workspace timeline kinds (stream messages, + * diffs, system rows, forum posts, job cards, huddle lifecycle). These kinds only + * exist on `block/buzz` relays, so their (verified) arrival IS the dialect + * detection. Attachment goes through the SAME shared NIP-29 path kind-9 chat uses + * — including its stray-redirect protection — so mixed vanilla/Buzz conversations + * share one timeline and non-host strays never mint phantom channels. + */ + private fun consumeBuzzTimelineEvent( + event: Event, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean = + consumeRegularEvent(event, relay, wasVerified).also { + markBuzzIfVerified(event, relay) + attachToRelayGroupIfScoped(event, relay) + } + + /** + * A Buzz kind-40099 system message. It renders as a narration row in the channel feed (via + * [consumeBuzzTimelineEvent]), but a `channel_deleted` one is also the **authoritative signal that + * a channel is gone**: the relay soft-deletes the channel and its 39000/39001/39002 discovery + * events but emits no member-removed notification and never retracts the kind-44100 that seeds the + * browse list — so without this the deleted channel keeps re-appearing (a stale 44100 re-announced + * every restart, its metadata now blank so it shows optimistically). Recording the delete in + * [RelayGroupDeletions] filters it out of every list and persists it across restarts. + * + * Gated on [isRelaySignedGroupEvent] (the 40099 is signed by the relay keypair) so a spoofed + * system message from a stray author can't hide a channel. Cross-device by construction: the relay + * replays this on subscribe, so a channel deleted on Buzz web/desktop is honored here too. + */ + private fun consume( + event: SystemMessageEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean = + consumeBuzzTimelineEvent(event, relay, wasVerified).also { + if (relay != null && isRelaySignedGroupEvent(event, relay) && event.payload()?.type == SystemMessagePayload.CHANNEL_DELETED) { + event.channel()?.let { channelId -> RelayGroupDeletions.markDeleted(GroupId(channelId, relay)) } + } + } + + /** Store-only consume for Buzz kinds that carry no channel timeline row. */ + private fun consumeBuzzRegularEvent( + event: Event, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean = + consumeRegularEvent(event, relay, wasVerified).also { + markBuzzIfVerified(event, relay) + } + + /** + * A Buzz forum ROOT post (kind 45001): a thread, not a chat message. Route it to the group's + * Threads collection ([RelayGroupChannel.addThread]) — the same place kind-11 threads go — so it + * surfaces in the forum/Threads view instead of leaking into the kind-9 chat feed as a bubble. + * Its kind-45003 comments are stored (store-only) and loaded on demand by the thread detail. + */ + private fun consumeBuzzForumPost( + event: Event, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean = + consumeRegularEvent(event, relay, wasVerified).also { + markBuzzIfVerified(event, relay) + attachThreadToRelayGroupIfScoped(event, relay) + } + + private fun consume( + event: StreamMessageEditEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean = + // Buzz's own timeline set excludes 40003: an edit is an OVERLAY replacing an + // earlier message's content, never a row of its own. Store it and anchor it to the + // message it edits (Note.edits) — like every other edit kind — so the overlay is held + // for as long as its message and never leaks into the timeline as a bubble. + consumeBuzzRegularEvent(event, relay, wasVerified).also { + val target = event.editedMessage() ?: return@also + val editNote = getOrCreateNote(event.id) + if (editNote.event != null) { + getOrCreateNote(target).addEdit(editNote) + } + } + + private fun consume( + event: DmVisibilityEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean = + // The relay-signed, `#p`-gated per-viewer hidden-DM snapshot (30622). Store the + // addressable event AND mirror the viewer's hidden set into the registry so a + // hidden DM drops out of that viewer's list until it's re-opened. + consumeBaseReplaceable(event, relay, wasVerified).also { + val viewer = event.viewer().ifBlank { return@also } + BuzzDmRegistry.recordHidden(viewer, event.hiddenChannels().toSet()) + } + + private fun consume( + event: CanvasEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean = + // The canvas is the channel's single living document, not a chat row: track + // only the newest revision as overlay state, never attach it to the timeline. + consumeBuzzRegularEvent(event, relay, wasVerified).also { + val channelId = event.channel() ?: return@also + val note = getOrCreateNote(event.id) + if (note.event != null) { + BuzzWorkspaceStates.getOrCreate(channelId).updateCanvas(note) + } + } + + /** + * A kind-44101 "you were removed from a channel". Stored like any other Buzz event and nothing more: + * withdrawing the matching add-prompt is not a side effect of ingest but a consequence of the stored + * event, since + * [com.vitorpamplona.amethyst.commons.model.buzz.BuzzChannelInvites.pendingInvites] resolves each + * channel to its newest verdict. That ordering is what makes the two kinds arriving out of order — + * routine on a re-subscribe, where the relay replays the whole history — produce the same answer as + * them arriving in order. + */ + private fun consume( + event: MemberRemovedNotificationEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean = consumeBuzzRegularEvent(event, relay, wasVerified) + + /** + * Attach a group-scoped content event (a kind-9 chat, kind-1068 poll, … + * carrying an `h` tag) to its [RelayGroupChannel]. NIP-29 reuses the generic + * content kinds and scopes them with `h`, so the note is consumed normally + * and then, when it belongs to a group and we know the serving relay, added + * to that group's channel timeline. + */ + private fun attachToRelayGroupIfScoped( + event: Event, + relay: NormalizedRelayUrl?, + ) { + val groupId = event.groupId() ?: return + val note = getOrCreateNote(event.id) + // Only attach a note we've actually loaded — never a placeholder for an + // unverified/not-yet-seen event. This is checked here (not via the "was + // newly consumed" flag) so the host relay's echo of an event we already + // stored from our own send still lands in the channel. + if (note.event == null) return + + if (relay != null) { + val exact = GroupId(groupId, relay) + val existing = getRelayGroupChannelIfExists(exact) + if (existing != null) { + // Normal arrival: the group's host-pinned filters served it, so the serving relay IS the + // group's key and its channel already exists. Fast O(1) path — no scan. + existing.addNote(note, relay) + } else { + // No channel keyed to the serving relay: this may be a stray from a NON-host relay (e.g. a + // quoted kind-9 resolved by id). Redirect it to the group's single confirmed host rather + // than mint a phantom channel the group's screens never read (the serving-relay hazard); + // fall back to the serving-relay key when there is no single host (new/ambiguous group). + val target = redirectStrayRelayGroupContent(relayGroupCandidatesFor(groupId)) ?: exact + getOrCreateRelayGroupChannel(target).addNote(note, relay) + } + } else { + // Our own optimistic send has no provenance relay, so we can't build the (groupId, + // relay) key. Attach only when a SINGLE open channel has this group id (the room being + // composed in). When the id is ambiguous across relays — e.g. the relay-wide "_" group + // joined on several relays — skip: attaching to all of them bleeds the message into + // rooms it wasn't sent to. The host relay's echo (relay != null) lands it on the right key. + relayGroupChannels + .filter { key, _ -> key.id == groupId } + .singleOrNull() + ?.addNote(note, null) + } + } + + /** Candidate group channels for the [redirectStrayRelayGroupContent] slow path — one scan by group id. */ + private fun relayGroupCandidatesFor(groupId: String): List = + relayGroupChannels + .filter { key, _ -> key.id == groupId } + .map { RelayGroupTargetCandidate(it.groupId, it.hasRelaySignedState()) } + + /** + * Same routing as [attachToRelayGroupIfScoped] but for kind-11 threads, which + * are kept in a separate collection from the chat timeline so the two content + * types don't mix in one feed. + */ + private fun attachThreadToRelayGroupIfScoped( + event: Event, + relay: NormalizedRelayUrl?, + ) { + val groupId = event.groupId() ?: return + val note = getOrCreateNote(event.id) + if (note.event == null) return + + if (relay != null) { + val exact = GroupId(groupId, relay) + val existing = getRelayGroupChannelIfExists(exact) + if (existing != null) { + existing.addThread(note) + } else { + // Same serving-relay hazard as the chat path: prefer the single confirmed host over a phantom. + val target = redirectStrayRelayGroupContent(relayGroupCandidatesFor(groupId)) ?: exact + getOrCreateRelayGroupChannel(target).addThread(note) + } + } else { + // See attachToRelayGroupIfScoped: only attach when the group id is unambiguous. + relayGroupChannels + .filter { key, _ -> key.id == groupId } + .singleOrNull() + ?.addThread(note) + } + } + + fun consume( + event: LiveActivitiesChatMessageEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val activityAddress = event.activityAddress() ?: return false + + val new = consumeRegularEvent(event, relay, wasVerified) + + if (new) { + val channel = getOrCreateLiveChannel(activityAddress) + val note = getOrCreateNote(event.id) + channel.addNote(note, relay) + } + + return new + } + + /** + * Audio-room presence (kind-10312) — addressable storage plus an + * author-keyed entry in the room's + * [LiveActivitiesChannel.presenceNotes] index. + * + * Presence is intentionally NOT added to `channel.notes`: that + * map is dominated by chat in active rooms and feeds that care + * about presence (Nests drawer, home live-bubble, NestsFeedLoaded) + * iterate `presenceNotes` directly for an O(speakers) scan. + * + * Cross-room move handling: kind-10312 is replaceable per author, + * but the room a presence points to (`a`-tag) can change when a + * speaker hops between rooms. The replaceable cache only swaps + * the addressable's content — it has no notion of which channel + * the old version was attached to. Without explicit eviction the + * old room would keep surfacing as "live" via the stale entry + * until it dropped out of the freshness window. Capture the prior + * room before replacement and, when it differs, drop the author + * from the old channel's presence index. + */ + fun consume( + event: MeetingRoomPresenceEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val priorVersion = getAddressableNoteIfExists(event.address())?.event as? MeetingRoomPresenceEvent + val priorRoomAddress = priorVersion?.interactiveRoom()?.address + val isReplacement = priorVersion != null && event.createdAt > priorVersion.createdAt + + val new = consumeBaseReplaceable(event, relay, wasVerified) + + val roomAddress = event.interactiveRoom()?.address ?: return new + if (roomAddress.kind != MeetingSpaceEvent.KIND) return new + + if (isReplacement && priorRoomAddress != null && priorRoomAddress != roomAddress) { + getLiveActivityChannelIfExists(priorRoomAddress)?.removePresenceNote(event.pubKey) + } + + val channel = getOrCreateLiveChannel(roomAddress) + val versionNote = getOrCreateNote(event.id) + channel.addPresenceNote(versionNote) + if (relay != null) channel.addRelay(relay) + + return new + } + + fun consume( + event: LiveActivitiesRaidEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val fromAddress = event.fromAddress() + val toAddress = event.toAddress() + if (fromAddress == null && toAddress == null) return false + + val new = consumeRegularEvent(event, relay, wasVerified) + + if (new) { + val note = getOrCreateNote(event.id) + fromAddress?.let { getOrCreateLiveChannel(it).addNote(note, relay) } + toAddress?.let { getOrCreateLiveChannel(it).addNote(note, relay) } + } + + return new + } + + fun consume( + event: LiveActivitiesClipEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val activityAddress = event.activityAddress() ?: return false + + val new = consumeRegularEvent(event, relay, wasVerified) + + if (new) { + val channel = getOrCreateLiveChannel(activityAddress) + val note = getOrCreateNote(event.id) + channel.addNote(note, relay) + } + + return new + } + + @Suppress("UNUSED_PARAMETER") + fun consume( + event: ChannelHideMessageEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean = false + + @Suppress("UNUSED_PARAMETER") + fun consume( + event: ChannelMuteUserEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean = false + + fun consume( + event: LnZapEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val note = getOrCreateNote(event.id) + + // Already processed this event — still ensure it's routed into any live-activity + // channel(s) it references. A zap that was first consumed by, e.g., the notifications + // subscription must still appear in the stream's chat when the user opens the stream. + if (note.event != null) { + attachZapToLiveActivityChannel(event, note, relay) + return false + } + + if (!(wasVerified || justVerify(event))) return false + + val existingZapRequest = event.zapRequest?.id?.let { getNoteIfExists(it) } + if (existingZapRequest == null || existingZapRequest.event == null) { + // tries to add it + event.zapRequest?.let { + checkDeletionAndConsume(it, relay, false) + } + } + + val zapRequest = event.zapRequest?.id?.let { getNoteIfExists(it) } + + if (zapRequest == null || zapRequest.event !is LnZapRequestEvent) { + Log.d("ZP") { "Zap Request not found. Unable to process Zap {${event.toJson()}}" } + return false + } + + // NIP-57 Appendix F validation. Resolve the recipient's lnurl from their + // profile metadata, look up the LNURL provider's `nostrPubkey`, and check + // the receipt against it (signer + invoice amount + lnurl tag). + // + // Synchronous path: cache hit, or no resolver wired (fallback to legacy + // signature-only behavior). Failed MUST-checks drop the receipt entirely. + // + // Async path: cache miss + resolver available. Load the event so feeds + // and live channels see it, but defer the `addZap` credit until the + // resolver returns. On async MUST-fail we never credit; the receipt + // stays in cache as a visible artifact but contributes 0 to zap totals. + val recipientLnurl = recipientLnurl(event) + val recipientLnurlpUrl = recipientLnurl?.let { LnurlForm.toUrl(it) } + val cachedInfo = recipientLnurlpUrl?.let { LnurlEndpointCache.get(it) } + + val author = getOrCreateUser(event.pubKey) + val repliesTo = computeReplyTo(event) + + if (cachedInfo != null) { + val result = + LnZapReceiptValidator.validate( + receipt = event, + expectedNostrPubkey = cachedInfo.nostrPubkey, + expectedLnurl = recipientLnurl, + ) + if (result is LnZapReceiptValidator.Result.Invalid && + result.reason != LnZapReceiptValidator.Result.Reason.MISMATCHED_LNURL + ) { + Log.w("ZP") { "dropping zap receipt ${event.id}: ${result.reason} ${result.detail ?: ""}" } + return false + } + if (result is LnZapReceiptValidator.Result.Invalid) { + Log.w("ZP") { "zap receipt ${event.id} has mismatched lnurl tag (accepting per SHOULD)" } + } + + note.loadEvent(event, author, repliesTo) + repliesTo.forEach { it.addZap(zapRequest, note) } + attachZapToLiveActivityChannel(event, note, relay) + refreshNewNoteObservers(note) + return true + } + + val resolver = lnurlEndpointResolver + if (resolver == null || recipientLnurlpUrl == null) { + // No resolver configured, or recipient has no lud16/lud06 on file. + // Legacy behavior: accept the receipt on signature verification alone. + note.loadEvent(event, author, repliesTo) + repliesTo.forEach { it.addZap(zapRequest, note) } + attachZapToLiveActivityChannel(event, note, relay) + refreshNewNoteObservers(note) + return true + } + + // Async validation. Make the event visible immediately, but only credit + // it to repliesTo after the resolver confirms the LNURL provider's pubkey. + note.loadEvent(event, author, repliesTo) + attachZapToLiveActivityChannel(event, note, relay) + refreshNewNoteObservers(note) + + appHost.scope.launch { + try { + val info = resolver.resolve(recipientLnurlpUrl) + if (info == null) { + Log.w("ZP") { "could not fetch lnurlp for ${event.id}; not crediting" } + return@launch + } + val result = + LnZapReceiptValidator.validate( + receipt = event, + expectedNostrPubkey = info.nostrPubkey, + expectedLnurl = recipientLnurl, + ) + if (result is LnZapReceiptValidator.Result.Invalid && + result.reason != LnZapReceiptValidator.Result.Reason.MISMATCHED_LNURL + ) { + Log.w("ZP") { "dropping zap receipt ${event.id}: ${result.reason} ${result.detail ?: ""}" } + return@launch + } + if (result is LnZapReceiptValidator.Result.Invalid) { + Log.w("ZP") { "zap receipt ${event.id} has mismatched lnurl tag (accepting per SHOULD)" } + } + repliesTo.forEach { it.addZap(zapRequest, note) } + } catch (t: Throwable) { + Log.w("ZP", "validation failed for ${event.id}", t) + } + } + + return true + } + + /** + * Look up the recipient's lnurl from their kind:0 metadata. The recipient is + * the receipt's first `p` tag (which the LNURL provider sets to the original + * zap target). Returns null if we have no metadata, or the user has neither + * lud16 nor lud06. + */ + private fun recipientLnurl(event: LnZapEvent): String? { + val recipientPubkey = event.zappedAuthor().firstOrNull() ?: return null + val user = getUserIfExists(recipientPubkey) ?: return null + return user.lnAddress()?.takeIf { it.isNotBlank() } + } + + fun consume( + event: OnchainZapEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val note = getOrCreateNote(event.id) + val alreadyLoaded = note.event != null + + // `relay == null` means this event was generated locally by the signed-in user + // and routed through `justConsumeMyOwnEvent` — see `Account.sendOnchainZap` → + // `LocalCache.justConsumeMyOwnEvent`. Only these get the optimistic gallery + // attachment; for incoming zaps from others, the sender-claimed `amount` tag + // is untrusted and could mislead the viewer until the chain verifier responds. + val isOwnEvent = relay == null + var repliesTo: List? = null + + if (!alreadyLoaded) { + if (!(wasVerified || justVerify(event))) return false + + // Anti-spoofing: NIP-BC requires rejecting self-zaps. + val recipient = event.recipient() ?: return false + if (event.pubKey.equals(recipient, ignoreCase = true)) return false + + val author = getOrCreateUser(event.pubKey) + val resolvedRepliesTo = computeReplyTo(event) + repliesTo = resolvedRepliesTo + note.loadEvent(event, author, resolvedRepliesTo) + + if (isOwnEvent) { + // Optimistic attachment for the sender's own zap: surface it on the + // thread immediately, before the chain backend has indexed the tx. + // Crucial because `OnchainZapSender.send` consumes the kind:8333 + // milliseconds after broadcasting the tx — the verifier would + // otherwise return TX_NOT_FOUND and the entry would never appear + // until a later re-verification pass. + val txid = event.txid() + if (txid != null) { + // Clamp claimedSats to a non-negative value. `amount` tag parses + // via toLongOrNull() with no sign check, so a malicious sender + // could otherwise put "-1" in the gallery as a negative-sats badge. + val claimedSats = (event.claimedAmountInSats() ?: 0L).coerceAtLeast(0L) + resolvedRepliesTo.forEach { + it.addOnchainZap(note, txid, claimedSats, verifiedSats = 0L, OnchainZapStatus.UNVERIFIED) + } + } + } + + refreshNewNoteObservers(note) + } + + // Async chain verification is delegated to OnchainZapResolver, which owns the + // in-flight gates (so two relay echoes don't double-fetch) and the chain-tip + // polling flow used by the gallery driver. Reusing the repliesTo already + // computed above avoids the second computeReplyTo pass on the new-event path. + onchainZapResolver.launchVerification(event, note, repliesTo ?: computeReplyTo(event)) + + return !alreadyLoaded + } + + fun consume( + event: Bolt12ZapEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val note = getOrCreateNote(event.id) + + // Already processed — still route it into any live-activity channel it references. + if (note.event != null) { + attachZapToLiveActivityChannel(event, note, relay) + return false + } + + if (!(wasVerified || justVerify(event))) return false + + // NIP-B1 validation is fully synchronous: zap-event structure, the embedded + // kind:9737 intent match, and the `lnp` payer-proof binding + crypto. A failed + // validation drops the zap entirely — it never contributes to a zap total. + // The outer event signature was already verified above (wasVerified/justVerify), + // so skip the redundant re-check inside the validator. + val validation = bolt12ZapValidator.validate(event, verifyEventSignature = false) + if (validation !is Bolt12ZapValidation.Valid) { + Log.w("ZP") { "dropping bolt12 zap ${event.id}: ${(validation as Bolt12ZapValidation.Invalid).reason}" } + return false + } + + val author = getOrCreateUser(event.pubKey) + val repliesTo = computeReplyTo(event) + note.loadEvent(event, author, repliesTo) + repliesTo.forEach { + it.addBolt12Zap(note, validation.paymentHashHex, validation.amountMillisats, validation.proofCryptoVerified) + } + attachZapToLiveActivityChannel(event, note, relay) + refreshNewNoteObservers(note) + return true + } + + /** + * Consume a NIP-61 nutzap (kind 9321). Resolves the e-tagged target + * note(s), parses the proof amounts once, and attaches a `NutzapEntry` + * so the reaction-row counter, the "you-already-zapped" icon highlight, + * and the dedicated cashu gallery row all light up the same way they + * do for lightning zaps. + * + * Unlike `consume(OnchainZapEvent)`, there's no async verification + * step today — the sender-claimed proof amounts are trusted up to + * redeem time, when the recipient's wallet verifies them against the + * mint. Adding a wallet-side verification gate that downgrades + * unverifiable entries would be a follow-up. + */ + fun consume( + event: NutzapEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val note = getOrCreateNote(event.id) + if (note.event != null) return false + + if (!(wasVerified || justVerify(event))) return false + + val author = getOrCreateUser(event.pubKey) + val repliesTo = computeReplyTo(event) + note.loadEvent(event, author, repliesTo) + + val claimedSats = event.claimedSatsTotal() + repliesTo.forEach { it.addNutzap(note, claimedSats) } + + refreshNewNoteObservers(note) + return true + } + + private fun attachZapToLiveActivityChannel( + event: LnZapEvent, + note: Note, + relay: NormalizedRelayUrl?, + ) { + // Match zap.stream: only show zaps whose receiver is the live activity host. + val hosts = event.zappedAuthor().toHashSet() + if (hosts.isEmpty()) return + + // Route into every live-activity address this zap references (zap.stream uses one, but + // a receipt could legitimately reference multiple simulcasted streams). + event.tags + .asSequence() + .mapNotNull(ATag::parseAddress) + .filter { it.kind == LiveActivitiesEvent.KIND && it.pubKeyHex in hosts } + .distinct() + .forEach { address -> + getOrCreateLiveChannel(address).addNote(note, relay) + } + } + + private fun attachZapToLiveActivityChannel( + event: Bolt12ZapEvent, + note: Note, + relay: NormalizedRelayUrl?, + ) { + // Only surface zaps whose recipient is the live activity host. + val host = event.recipient() ?: return + event.tags + .asSequence() + .mapNotNull(ATag::parseAddress) + .filter { it.kind == LiveActivitiesEvent.KIND && it.pubKeyHex == host } + .distinct() + .forEach { address -> + getOrCreateLiveChannel(address).addNote(note, relay) + } + } + + fun consume( + event: LnZapRequestEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val note = getOrCreateNote(event.id) + + // Already processed this event. + if (note.event != null) return false + + if (wasVerified || justVerify(event)) { + val author = getOrCreateUser(event.pubKey) + val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } + val repliesTo = computeReplyTo(event) + + note.loadEvent(event, author, repliesTo) + + repliesTo.forEach { it.addZap(note, null) } + + refreshNewNoteObservers(note) + + return true + } + + return false + } + + fun consume( + event: TextNoteModificationEvent, + 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()) + + // Anchor the modification to the note it edits, like every other edit kind — the read side + // (Note.textNoteModifications) then folds `edited.edits` instead of scanning the cache, + // and addEdit invalidates the note's edits flow so the UI re-derives. + event.editedNote()?.let { + checkGetOrCreateNote(it.eventId)?.addEdit(note) + } + + refreshNewNoteObservers(note) + + return true + } + + return false + } + + fun consume( + event: ConcordChatEditEvent, + 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 + + // A Concord edit rumor is unsigned (its sig is empty); the envelope open path already + // established authenticity, so we consume it as pre-verified like any other Concord rumor. + if (wasVerified || justVerify(event)) { + note.loadEvent(event, author, emptyList()) + + // Anchor the edit to the message it edits (like a reaction to its target), so it survives + // as long as that channel-retained message does. A Concord rumor is decrypted exactly once + // per session — the community session dedups re-delivered wraps — so an edit left orphaned + // in the soft cache could be GC'd and never re-downloaded. The bubble reads `note.edits`. + event.editedMessageId()?.let { targetId -> + getOrCreateNote(targetId).addEdit(note) + } + + refreshNewNoteObservers(note) + + return true + } + + return false + } + + fun consume( + event: PollResponseEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val pollId = event.poll()?.eventId + if (pollId != null) { + val pollNote = getOrCreateNote(pollId) + val responseNote = getOrCreateNote(event.id) + + val new = consumeRegularEvent(event, relay, wasVerified) + if (new) { + pollNote.pollState().addResponse(responseNote) + // Responses and their poll race each other. If the poll is already here, hand the + // tally its rules now; if it isn't, consume(PollEvent) does it on arrival. + (pollNote.event as? PollEvent)?.let { + pollNote.pollState().updatePolicy(PollTallyPolicy.from(it)) + } + } + return new + } + + return false + } + + fun consume( + event: PollEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val new = consumeRegularEvent(event, relay, wasVerified) + attachToRelayGroupIfScoped(event, relay) + + // Not gated on `new`: the tally may have been built from responses that arrived before this + // poll did, and updatePolicy is idempotent for the usual re-delivery from another relay. + getOrCreateNote(event.id).pollState().updatePolicy(PollTallyPolicy.from(event)) + + return new + } + + fun consume( + event: FileStorageEvent, + 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) + } + + // A host with no blob directory keeps the bytes in memory below instead of spilling them. + val cachePath = appHost.nip95BlobDir + + val isVerified = + if (cachePath == null) { + wasVerified + } else { + try { + cachePath.mkdirs() + val file = File(cachePath, event.id) + if (!file.exists() && (wasVerified || justVerify(event))) { + FileOutputStream(file).use { stream -> + stream.write(event.decode()) + } + Log.i( + "FileStorageEvent", + "NIP95 File received from $relay and saved to disk as $file", + ) + true + } else { + wasVerified + } + } catch (e: IOException) { + Log.e("FileStorageEvent", "FileStorageEvent save to disk error: " + event.id, e) + wasVerified + } + } + + // Already processed this event. + if (note.event != null) return false + + if (isVerified || justVerify(event)) { + // The blob is on disk now, so the copy in the note drops its content. Without a + // directory to spill to there is nowhere else for it to live, so keep it whole. + val stored = + if (cachePath != null) { + FileStorageEvent(event.id, event.pubKey, event.createdAt, event.tags, "", event.sig) + } else { + event + } + + note.loadEvent(stored, author, emptyList()) + + refreshNewNoteObservers(note) + + return true + } + + return false + } + + fun consume( + event: LnZapPaymentRequestEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + // Does nothing without a response callback. + return true + } + + fun consume( + event: LnZapPaymentRequestEvent, + zappedNote: Note?, + wasVerified: Boolean, + relay: NormalizedRelayUrl?, + onResponse: suspend (LnZapPaymentResponseEvent) -> Unit, + ): Boolean { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + // Already processed this event. + if (note.event != null) return false + + if (wasVerified || justVerify(event)) { + val expectedServicePubkey = + event.walletServicePubKey() ?: run { + Log.w("LocalCache") { "NWC request ${event.id} has no `p` tag; cannot register for response." } + return false + } + + note.loadEvent(event, author, emptyList()) + + relay?.let { + note.addRelay(relay) + } + + zappedNote?.addZapPayment(note, null) + + paymentTracker.registerRequest(event.id, expectedServicePubkey, zappedNote, onResponse) + + refreshNewNoteObservers(note) + + return true + } + + return false + } + + fun consume( + event: LnZapPaymentResponseEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val requestId = event.requestId() + + // Duplicate delivery, checked before the tracker so the warnings below mean one + // thing each. Some NWC relays replay every cached kind-23195 whenever the REQ + // filter changes (see NWCPaymentFilterAssembler), so an already-answered response + // arrives again and again. Its first copy consumed the pending request, so the + // replays would otherwise be reported as "no pending request is registered" — + // the same line a genuinely late response produces, which made the two + // indistinguishable in the field. + if (getNoteIfExists(event.id)?.event != null) return false + + val pending = + when (val match = paymentTracker.onResponseReceived(requestId, event.pubKey)) { + is NwcPaymentTracker.MatchResult.Matched -> { + match.pending + } + + is NwcPaymentTracker.MatchResult.WrongAuthor -> { + // Possible spoof: a kind-23195 event from someone other than + // the wallet service we sent the request to. The pending + // entry is left in place so the real response can still + // resolve it; we silently drop this one. + Log.w("LocalCache") { + "Rejecting NWC response ${event.id}: expected author ${match.expected} but event was signed by ${match.actual}. " + + "This may be a spoofed reply — keeping the request pending for the legitimate wallet response." + } + return false + } + + NwcPaymentTracker.MatchResult.NoMatch -> { + // Not a replay — those are filtered above — so this is the first time we + // have seen this response and nothing is waiting for it. + Log.w("LocalCache") { + "NWC response ${event.id} from ${event.pubKey} references request e=$requestId but no pending request is registered. " + + "The response arrived after the client gave up waiting, the user holds a stale subscription, " + + "or the wallet service set the wrong e tag." + } + return false + } + } + + val zappedNote = pending.zappedNote + val responseCallback = pending.onResponse + + val requestNote = requestId?.let { checkGetOrCreateNote(requestId) } + + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + // Backstop for a concurrent delivery that loaded the event between the replay + // check above and here. Same outcome, no warning: it is not a protocol problem. + if (note.event != null) return false + + if (wasVerified || justVerify(event)) { + note.loadEvent(event, author, emptyList()) + + requestNote?.let { request -> zappedNote?.addZapPayment(request, note) } + + appHost.scope.launch { + responseCallback(event) + } + + return true + } + + return false + } + + fun getPeopleListNotesFor(user: User): List = addressables.filter(PeopleListEvent.KIND, user.pubkeyHex) + + override fun markAsSeen( + eventId: String, + relay: NormalizedRelayUrl, + ) { + val note = getNoteIfExists(eventId) + + note?.event?.let { noteEvent -> + if (noteEvent is AddressableEvent) { + getAddressableNoteIfExists(noteEvent.address())?.addRelay(relay) + } + } + + note?.let { addRelayToNoteAndInners(it, relay) } + } + + /** + * Adds [relay] to [note] and to every already-unwrapped inner note of its + * gift-wrap chain (wrap → seal → rumor). The chat UI renders the inner + * rumor, so a relay recorded only on the outer envelope never surfaces as + * an icon. Inner notes that don't exist yet are not lost: the unwrap path + * copies the envelope's relays down via [copyRelaysFromTo] when it runs. + */ + fun addRelayToNoteAndInners( + note: Note, + relay: NormalizedRelayUrl, + ) { + note.addRelay(relay) + + val noteEvent = note.event + if (noteEvent is HasInnerEvent) { + noteEvent.innerEventId?.let { innerId -> + getNoteIfExists(innerId)?.let { addRelayToNoteAndInners(it, relay) } + } + } + } + + // Observers line up here. + val live: LocalCacheFlow = LocalCacheFlow() + + private fun refreshNewNoteObservers(newNote: Note) { + val event = newNote.event as Event + + // Index-driven fanout: only observers whose filter narrows + // on a field this event carries (or that registered as + // unindexed) get woken up. The match check inside each + // observer's `new()` still enforces negative constraints. + for (observer in observables.candidatesFor(event)) { + observer.new(event, newNote) + } + live.newNote(newNote) + } + + internal fun refreshDeletedNoteObservers(newNote: Note) { + // Deletes don't have a filterable shape — every observer + // might hold this note in its result set, so iterate them + // all. The index doesn't help here. + observables.forEach { it.remove(newNote) } + live.removedNote(newNote) + } + + /** + * Resource-usage ledger hook: called with (elapsedNanos, valid) for every + * signature verification so the app can account crypto CPU per day. + * Wired by AppModules like [onchainBackend]; null costs nothing. + */ + @Volatile + var verifyMeter: ((elapsedNanos: Long, valid: Boolean) -> Unit)? = null + + fun justVerify(event: Event): Boolean { + appHost.assertNotMainThread() + + val meter = verifyMeter + if (meter == null) return justVerifyInner(event) + + val start = System.nanoTime() + val valid = justVerifyInner(event) + meter(System.nanoTime() - start, valid) + return valid + } + + private fun justVerifyInner(event: Event): Boolean = + if (!event.verify()) { + try { + 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}" } + } + false + } else { + true + } + + fun consume( + event: DraftWrapEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean = !event.isDeleted() && consumeBaseReplaceable(event, relay, wasVerified) + + override fun consumeEmbedded(event: Event) { + justConsume(event, null, false) + } + + override fun justConsumeMyOwnEvent(event: Event) = justConsumeAndUpdateIndexes(event, null, true) + + fun justConsume( + event: Event, + relay: IRelayClient?, + wasVerified: Boolean, + ): Boolean { + if (deletionIndex.hasBeenDeleted(event)) { + // update relay with deletion event from another. + if (relay != null) { + deletionIndex.hasBeenDeletedBy(event)?.let { deletionEvent -> + getNoteIfExists(deletionEvent.id)?.let { note -> + if (!note.hasRelay(relay.url)) { + if (appHost.isDebug) { + Log.d("LocalCache") { "Updating ${relay.url.url} with a Deletion Event ${event.id} ${deletionEvent.id} because of ${event.toJson()} with ${deletionEvent.toJson()}" } + } + relay.sendIfConnected(EventCmd(deletionEvent)) + note.addRelay(relay.url) + } + } + } + } + return false + } + + if (event is AddressableEvent && relay != null) { + // updates relay with a new event. + getAddressableNoteIfExists(event.address())?.let { note -> + note.event?.let { existingEvent -> + if (existingEvent.createdAt > event.createdAt && !note.hasRelay(relay.url) && !deletionIndex.hasBeenDeleted(event) && !event.isExpired()) { + if (appHost.isDebug) { + Log.d("LocalCache") { "Updating ${relay.url.url} with a new version of ${event.kind} ${event.id} to ${existingEvent.id}" } + } + + relay.sendIfConnected(EventCmd(existingEvent)) + // only send once. + note.addRelay(relay.url) + } + } + } + } + + return justConsumeAndUpdateIndexes(event, relay?.url, wasVerified) + } + + fun checkDeletionAndConsume( + event: Event, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean = + if (!deletionIndex.hasBeenDeleted(event)) { + justConsumeAndUpdateIndexes(event, relay, wasVerified) + } else { + false + } + + private fun justConsumeAndUpdateIndexes( + event: Event, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + // uses the internal event to avoid reprocessing cached items. + val note = + if (event is AddressableEvent) { + getOrCreateAddressableNote(event.address()) + } else { + getOrCreateNote(event.id) + } + + val wasNew = justConsumeInnerInner(event, relay, wasVerified) + + if (wasNew) { + updateHintIndexes(event) + updateMintIndex(event) + updateInGatherer(event, note) + } + + if (relay != null) { + note.event?.let { consumedEvent -> + addIncomingRelayAsHintToAllRelatedEvents(consumedEvent, 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 { + 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) + } + } + } + + /** + * Feeds the Cashu mint URL directory from every event that names a + * mint, regardless of which user authored it. Three sources today: + * + * - NutzapInfoEvent (kind:10019) — every nostr user with a Cashu + * wallet publishes their accepted mints here, so a typical inbox + * of cached profiles seeds a useful starter directory. + * - MintRecommendationEvent (kind:38000) — explicit public vouches. + * - CashuMintEvent (kind:38172) — formal mint announcements. + * + * Called only when the event is newly consumed (mirrors + * updateHintIndexes) so a re-emission of a cached event doesn't + * inflate the popularity counter. + */ + fun updateMintIndex(event: Event) { + when (event) { + is NutzapInfoEvent -> mintDirectory.addAll(event.mints().map { it.mintUrl }) + is MintRecommendationEvent -> mintDirectory.addAll(event.mintUrls()) + is CashuMintEvent -> event.mintUrl()?.let { mintDirectory.add(it) } + else -> Unit + } + } + + fun updateInGatherer( + event: Event, + note: Note, + ) { + if (event is EventHintProvider) { + event.linkedEventIds().forEach { + checkGetOrCreateNote(it)?.addGatherer(note) + } + } + if (event is AddressHintProvider) { + event.linkedAddressIds().forEach { + checkGetOrCreateAddressableNote(it)?.addGatherer(note) + } + } + } + + @Suppress("DEPRECATION") + private fun justConsumeInnerInner( + event: Event, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean = + try { + when (event) { + // ============================================================ + // Kinds with dedicated consume() logic: typed overloads that + // update channels, zaps, drafts, the deletion index, etc. + // Everything without per-kind logic falls through to the + // generic replaceable / regular groups at the bottom. + // ============================================================ + + is NutzapEvent -> consume(event, relay, wasVerified) + is ChannelCreateEvent -> consume(event, relay, wasVerified) + is ChannelHideMessageEvent -> consume(event, relay, wasVerified) + is ChannelMessageEvent -> consume(event, relay, wasVerified) + is ChannelMetadataEvent -> consume(event, relay, wasVerified) + is ChannelMuteUserEvent -> consume(event, relay, wasVerified) + is CommunityPostApprovalEvent -> consume(event, relay, wasVerified) + is DeletionEvent -> consume(event, relay, wasVerified) + is DraftWrapEvent -> consume(event, relay, wasVerified) + is EphemeralChatEvent -> consume(event, relay, wasVerified) + is GeohashChatEvent -> consume(event, relay, wasVerified) + is GroupMetadataEvent -> consume(event, relay, wasVerified) + is GroupMembersEvent -> consume(event, relay, wasVerified) + is GroupAdminsEvent -> consume(event, relay, wasVerified) + is GroupPinnedEvent -> consume(event, relay, wasVerified) + + // 39003 (relay-declared roles) is durable group state like 39000/39001/39002: + // route it onto the channel so a moderation UI can offer the relay's role set. + is SupportedRolesEvent -> consume(event, relay, wasVerified) + + is GenericRepostEvent -> consume(event, relay, wasVerified) + is FileStorageEvent -> consume(event, relay, wasVerified) + + is GiftWrapEvent -> { + // A wrap with an empty content carries no NIP-44 ciphertext and can + // never be unwrapped — reject it before paying for a signature check + // and a cache slot. Locally stripped copies (copyNoContent) are + // assigned straight to note.event and never pass through here. + if (event.content.isEmpty()) { + false + } else { + consumeRegularEvent(event, relay, wasVerified) + } + } + + is LiveActivitiesEvent -> consume(event, relay, wasVerified) + is LiveActivitiesChatMessageEvent -> consume(event, relay, wasVerified) + is LiveActivitiesRaidEvent -> consume(event, relay, wasVerified) + is LiveActivitiesClipEvent -> consume(event, relay, wasVerified) + is MeetingRoomPresenceEvent -> consume(event, relay, wasVerified) + + is PodcastMetadataEvent -> { + // Drop the known "Mock Podcast" spam flood instead of caching thousands of them. + if (event.isMockSpam()) { + false + } else { + consumeBaseReplaceable(event, relay, wasVerified) + } + } + + is LnZapEvent -> consume(event, relay, wasVerified) + is LnZapRequestEvent -> consume(event, relay, wasVerified) + is OnchainZapEvent -> consume(event, relay, wasVerified) + is Bolt12ZapEvent -> consume(event, relay, wasVerified) + is LnZapPaymentRequestEvent -> consume(event, relay, wasVerified) + is LnZapPaymentResponseEvent -> consume(event, relay, wasVerified) + is LongTextNoteEvent -> consume(event, relay, wasVerified) + is MetadataEvent -> consume(event, relay, wasVerified) + is NipTextEvent -> consume(event, relay, wasVerified) + is OtsEvent -> consume(event, relay, wasVerified) + is PollResponseEvent -> consume(event, relay, wasVerified) + is ReactionEvent -> consume(event, relay, wasVerified) + is LabelEvent -> consume(event, relay, wasVerified) + is ContactCardEvent -> consume(event, relay, wasVerified) + is ReportEvent -> consume(event, relay, wasVerified) + is RepostEvent -> consume(event, relay, wasVerified) + is StatusEvent -> consume(event, relay, wasVerified) + is TextNoteModificationEvent -> consume(event, relay, wasVerified) + is ConcordChatEditEvent -> consume(event, relay, wasVerified) + is WikiNoteEvent -> consume(event, relay, wasVerified) + is PaymentTargetsEvent -> consume(event, relay, wasVerified) + + is ChatEvent -> { + consumeRegularEvent(event, relay, wasVerified).also { + // Attach on every arrival, not just the newly-consumed one: + // our own send is consumed first with a null relay, so the + // host relay's later echo (new == false) is what carries the + // provenance needed to key the channel. attach is idempotent. + attachToRelayGroupIfScoped(event, relay) + } + } + + is PollEvent -> consume(event, relay, wasVerified) + + is ThreadEvent -> { + consumeRegularEvent(event, relay, wasVerified).also { + attachThreadToRelayGroupIfScoped(event, relay) + } + } + + // ------------------------------------------------------------------ + // Buzz workspace kinds (block/buzz — the Buzz dialect of NIP-29). + // Timeline kinds attach into the group's BuzzWorkspaceChannel; the + // rest are stored for query/state. Kinds 9041/39005/49001 are absent + // on purpose: their numbers belong to GoalEvent, GroupPinnedEvent and + // a non-wire audit kind. Kind 20001 is shared with GeohashPresenceEvent + // (BitChat); EventFactory routes it to PresenceUpdateEvent only when the + // BitChat `g` tag is absent, and it is handled below with the ephemerals. + // ------------------------------------------------------------------ + + is StreamMessageEditEvent -> consume(event, relay, wasVerified) + is SystemMessageEvent -> consume(event, relay, wasVerified) + is CanvasEvent -> consume(event, relay, wasVerified) + is MemberRemovedNotificationEvent -> consume(event, relay, wasVerified) + is RelayMembershipListEvent -> consume(event, relay, wasVerified) + is RelayAddMemberEvent -> consume(event, relay, wasVerified) + is RelayRemoveMemberEvent -> consume(event, relay, wasVerified) + is DmVisibilityEvent -> consume(event, relay, wasVerified) + + // Forum root (45001) is a thread, not a chat row → Threads collection. Comments (45003) + // and votes (45002) are store-only: the forum-thread detail loads them on demand by root. + is ForumPostEvent -> consumeBuzzForumPost(event, relay, wasVerified) + + is StreamMessageV2Event, + is StreamMessageDiffEvent, + is JobRequestEvent, + is JobAcceptedEvent, + is JobProgressEvent, + is JobResultEvent, + is JobCancelEvent, + is JobErrorEvent, + is HuddleStartedEvent, + is HuddleParticipantJoinedEvent, + is HuddleParticipantLeftEvent, + is HuddleEndedEvent, + -> consumeBuzzTimelineEvent(event, relay, wasVerified) + + // Buzz store-only regular kinds (queryable state; no timeline row yet), + // plus relay-signed sidecars and audit projections. + is ForumCommentEvent, + is ForumVoteEvent, + is StreamMessagePinnedEvent, + is StreamMessageBookmarkedEvent, + is StreamMessageScheduledEvent, + is StreamReminderEvent, + is DmCreatedEvent, + is DmOpenEvent, + is DmAddMemberEvent, + is DmHideEvent, + is MemberAddedNotificationEvent, + is AgentTurnMetricEvent, + is ModerationBanEvent, + is ModerationTimeoutEvent, + is ModerationUntimeoutEvent, + is ModerationResolveReportEvent, + is ProductFeedbackEvent, + is RelayAdminAddMemberEvent, + is RelayAdminRemoveMemberEvent, + is RelayAdminChangeRoleEvent, + is SetWorkspaceProfileEvent, + is ArchiveRequestEvent, + is UnarchiveRequestEvent, + is ArchivedIdentityEvent, + is UnarchivedIdentityEvent, + is HuddleGuidelinesEvent, + is WorkflowTriggeredEvent, + is WorkflowStepStartedEvent, + is WorkflowStepCompletedEvent, + is WorkflowStepFailedEvent, + is WorkflowCompletedEvent, + is WorkflowFailedEvent, + is WorkflowCancelledEvent, + is WorkflowApprovalRequestedEvent, + is WorkflowApprovalGrantedEvent, + is WorkflowApprovalDeniedEvent, + is WorkflowTriggerEvent, + is ApprovalGrantEvent, + is ApprovalDenyEvent, + is AuditEntryEvent, + is ChannelSummaryEvent, + is PresenceSnapshotEvent, + -> consumeBuzzRegularEvent(event, relay, wasVerified) + + // Buzz ephemeral signals: transient by definition (20000-29999) — do not + // pollute the note store, and do NOT mark the dialect from them (they are + // never stored, so the verified-mark gate has nothing to check). + is TypingIndicatorEvent -> { + // Live "who is typing" side-effect only — record the heartbeat into the + // process-wide typing state (the channel view collects it) and return + // false so it never becomes a feed row. Own-typing is filtered in the UI. + event.channelId()?.let { BuzzTypingState.record(it, event.pubKey, event.createdAt, TimeUtils.now()) } + false + } + is PresenceUpdateEvent -> { + // Live online/away/offline side-effect only — record the latest status for + // the subject (the `p` tag on relay-synthesized reads, else the author) into + // the process-wide presence state and return false so it never becomes a row. + BuzzPresenceState.record(event.subjectPubKey(), event.status(), event.createdAt) + false + } + is ObserverFrameEvent -> false + is HuddleReactionEvent -> false + // Pairing (24134) is deliberately dialect-neutral: it flows during device + // pairing before any workspace relationship is established. + is PairingEvent -> false + + // ============================================================ + // Replaceable / addressable kinds with no per-kind logic: + // the newest version per address is kept, older ones dropped. + // New kinds without custom consume logic go in one of the two + // groups below — an unlisted kind falls into the else branch + // and is rejected as unsupported. + // ============================================================ + is AcceptedBadgeSetEvent, + is AdvertisedRelayListEvent, + is AppDefinitionEvent, + is AppRecommendationEvent, + is AppSpecificDataEvent, + is AttestationEvent, + is AttestationRequestEvent, + is AttestorRecommendationEvent, + is AttestorProficiencyEvent, + is AudioTrackEvent, + is BadgeDefinitionEvent, + is BlockedRelayListEvent, + is BlossomServersEvent, + is NestsServersEvent, + is BroadcastRelayListEvent, + is BookmarkListEvent, + is OldBookmarkListEvent, + is CalendarEvent, + is CalendarDateSlotEvent, + is CalendarTimeSlotEvent, + is CalendarRSVPEvent, + is CashuWalletEvent, + is NutzapInfoEvent, + is ChannelListEvent, + is ChatMessageRelayListEvent, + is Bolt12OfferListEvent, + is ClassifiedsEvent, + is FundraiserEvent, + is BirdexEvent, + is Ps1SaveEvent, + is CommunityDefinitionEvent, + is CommunityListEvent, + is ContactListEvent, + // DVM heartbeat (11998): stored per Address(11998, author, d) so liveness checks find the + // beat at the announcement's mirror address (amethyst/plans/2026-09-10-dvm-heartbeat-liveness.md), + // AND recorded into the strong registry — beat notes are WeakReference-held with no strong + // holder on the Discover screen, so the gate must not depend on them surviving GC. + is DvmHeartbeatEvent, + -> + consumeBaseReplaceable(event, relay, wasVerified).also { + DvmHeartbeatRegistry.record(event.address(), event.createdAt) + } + is EmojiPackEvent, + is EmojiPackSelectionEvent, + is EphemeralChatListEvent, + // NIP-51 "simple groups" list (kind 10009): the user's joined NIP-29 groups + + // servers. Replaceable like its sibling lists; RelayGroupListState reads it from the + // addressable cache, so it must be stored (it was silently dropped before). + is SimpleGroupListEvent, + // Concord private joined-communities list (kind 13302). Replaceable, self-encrypted; + // ConcordChannelListState observes it via the addressable cache (Address(13302, me, "")), + // so — exactly like the 10009 list above — it must be stored replaceably or the Concord + // hub stays empty even after the event arrives. + is ConcordCommunityListEvent, + // The relay-signed NIP-29 39004 AV-participants addressable is durable group state. + is GroupParticipantsEvent, + is ExternalIdentitiesEvent, + is FileServersEvent, + is FollowListEvent, + is GeohashListEvent, + is GitRepositoryEvent, + is GitRepositoryStateEvent, + is UserGraspListEvent, + is RootSiteEvent, + is NamedSiteEvent, + is RootNappletEvent, + is NamedNappletEvent, + is RelayFeedsListEvent, + is KeyPackageEvent, + is KeyPackageRelayListEvent, + is LiveChessGameChallengeEvent, + is LiveChessGameAcceptEvent, + is LiveChessMoveEvent, + is LiveChessGameEndEvent, + is LiveChessDrawOfferEvent, + is HashtagListEvent, + is FavoriteAlgoFeedsListEvent, + is IndexerRelayListEvent, + is InteractiveStoryPrologueEvent, + is InteractiveStorySceneEvent, + is InteractiveStoryReadingStateEvent, + is InterestSetEvent, + is LabeledBookmarkListEvent, + is MeetingSpaceEvent, + is MeetingRoomEvent, + is MusicTrackEvent, + is MusicPlaylistEvent, + is AuthoredPodcastsEvent, + is FavoritePodcastsListEvent, + is Podcasting20EpisodeEvent, + is Podcasting20TrailerEvent, + is MuteListEvent, + is NNSEvent, + is PrivateOutboxRelayListEvent, + is ProfileBadgesEvent, + is ProxyRelayListEvent, + is PinListEvent, + is PeopleListEvent, + // Buzz addressable/replaceable state. + is PersonaEvent, + is TeamEvent, + is ManagedAgentEvent, + is AgentProfileEvent, + is EngramEvent, + is WorkflowDefEvent, + is EventReminderEvent, + is PushLeaseEvent, + is WindowBoundsEvent, + is ArchivedIdentitiesListEvent, + is RelayDiscoveryEvent, + is RelayMonitorEvent, + is RelaySetEvent, + is ReleaseArtifactSetEvent, + is SearchRelayListEvent, + is SoftwareApplicationEvent, + is TrustedRelayListEvent, + is TrustProviderListEvent, + is VideoHorizontalEvent, + is VideoVerticalEvent, + is WebBookmarkEvent, + is ExerciseTemplateEvent, + is PublicationIndexEvent, + is WikiRedirectEvent, + is LearningResourceEvent, + is BookshelfDirectoryEvent, + is BlossomPieceIndexEvent, + is PublicationContentEvent, + is RelayReviewEvent, + is EntityRatingEvent, + -> consumeBaseReplaceable(event, relay, wasVerified) + + // ============================================================ + // Regular kinds with no per-kind logic: stored as plain notes. + // ============================================================ + is AudioHeaderEvent, + is BadgeAwardEvent, + is CallAnswerEvent, + is CallHangupEvent, + is CallIceCandidateEvent, + is CallOfferEvent, + is CallRejectEvent, + is CallRenegotiateEvent, + is CashuTokenEvent, + is CashuSpendingHistoryEvent, + is CashuMintQuoteEvent, + // NIP-87 Cashu mint discovery + recommendations: all three are kind 3xxxx + // (parameterized-replaceable per the spec) but neither CashuMintEvent / + // FedimintEvent / MintRecommendationEvent extends AddressableEvent in Quartz + // today, so consumeBaseReplaceable's `check(event is AddressableEvent)` would + // crash. Route them as regular events — downstream consumers + // (CashuMintDirectoryState, CashuWalletState) already dedupe by (pubKey, dTag) + // and keep the newest. + is CashuMintEvent, + is FedimintEvent, + is MintRecommendationEvent, + is ChatMessageEncryptedFileHeaderEvent, + is ChatMessageEvent, + is BirdDetectionEvent, + is CommentEvent, + // The NIP-29 9xxx moderation actions and join/leave requests are regular + // one-shot events the relay is authoritative for (it applies them and + // republishes the 39000/39001/39002); we store them so they're queryable, + // but we don't act on them client-side. + is PutUserEvent, + is RemoveUserEvent, + is EditMetadataEvent, + is DeleteEventEvent, + is UpdatePinListEvent, + is DeleteGroupEvent, + is CreateGroupEvent, + is CreateInviteEvent, + is JoinRequestEvent, + is LeaveRequestEvent, + is FhirResourceEvent, + is FileHeaderEvent, + is ProfileGalleryEntryEvent, + is FileStorageHeaderEvent, + is GoalEvent, + is GroupEvent, + is GitIssueEvent, + is GitReplyEvent, + is GitPatchEvent, + is GitPullRequestEvent, + is GitPullRequestUpdateEvent, + is GitStatusEvent, + is ChessGameEvent, + is JesterEvent, + is HighlightEvent, + is PodcastEpisodeEvent, + is NIP90StatusEvent, + is NIP90ContentDiscoveryResponseEvent, + is NIP90ContentDiscoveryRequestEvent, + is NIP90UserDiscoveryResponseEvent, + is NIP90UserDiscoveryRequestEvent, + is PictureEvent, + is PrivateDmEvent, + is PublicMessageEvent, + is RequestToVanishEvent, + is CodeSnippetEvent, + is ZapPollEvent, + is RoadEventReportEvent, + is RoadEventConfirmationEvent, + is SealedRumorEvent, + is SoftwareAssetEvent, + is ExternalReactionEvent, + is ExternalCitationEvent, + is HardcopyCitationEvent, + is PromptCitationEvent, + is WikiMergeRequestEvent, + is WikiMergeAcceptanceEvent, + is TextNoteEvent, + is TorrentEvent, + is TorrentCommentEvent, + is VideoNormalEvent, + is VideoShortEvent, + is VoiceEvent, + is VoiceReplyEvent, + is WakeUpEvent, + is WelcomeEvent, + is WorkoutRecordEvent, + -> consumeRegularEvent(event, relay, wasVerified) + + else -> { + Log.w("Event Not Supported") { "From ${relay?.url}: ${event.toJson()}" }.let { false } + } + } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.w("LocalCache", "Cannot consume ${event.toJson()} from ${relay?.url}", e) + false + } + + fun hasConsumed(notificationEvent: Event): Boolean = + if (notificationEvent is AddressableEvent) { + val note = addressables.get(notificationEvent.address()) + val noteEvent = note?.event + noteEvent != null && notificationEvent.createdAt <= noteEvent.createdAt + } else { + val note = notes.get(notificationEvent.id) + note?.event != null + } + + fun copyRelaysFromTo( + from: Note, + to: Event, + ) { + val toNote = getOrCreateNote(to) + from.relays.forEach { + toNote.addRelay(it) + } + } + + fun copyRelaysFromTo( + from: Note, + to: HexKey, + ) { + val toNote = getOrCreateNote(to) + from.relays.forEach { + toNote.addRelay(it) + } + } +} + +@Stable +class LocalCacheFlow { + 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.IO) + + // Refreshes observers in batches. + private val bundler2 = BundledInsert(1000, Dispatchers.IO) + + fun newNote(newNote: Note) { + bundler.invalidateList(newNote) { bundledNewNotes -> + _newEventBundles.emit(bundledNewNotes) + } + } + + fun removedNote(newNote: Note) { + bundler2.invalidateList(newNote) { bundledNewNotes -> + _deletedEventBundles.emit(bundledNewNotes) + } + } +} diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCache.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCache.kt index 5f4adbaf49..44eb87c74b 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCache.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCache.kt @@ -18,4007 +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. */ -@file:Suppress("DEPRECATION") - package com.vitorpamplona.amethyst.commons.model.cache -import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.cashu.MintDirectoryIndex -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.commons.model.Dao -import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.OnchainZapStatus -import com.vitorpamplona.amethyst.commons.model.RelayGroupTargetCandidate -import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.commons.model.UserContext -import com.vitorpamplona.amethyst.commons.model.buzz.BuzzCommunityMembership -import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmRegistry -import com.vitorpamplona.amethyst.commons.model.buzz.BuzzPresenceState -import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect -import com.vitorpamplona.amethyst.commons.model.buzz.BuzzTypingState -import com.vitorpamplona.amethyst.commons.model.buzz.BuzzWorkspaceStates -import com.vitorpamplona.amethyst.commons.model.cache.LocalCache.observeEvents -import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel -import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel -import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel -import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupDeletions -import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel -import com.vitorpamplona.amethyst.commons.model.nip88Polls.PollTallyPolicy -import com.vitorpamplona.amethyst.commons.model.nip90DVMs.DvmHeartbeatRegistry -import com.vitorpamplona.amethyst.commons.model.nipBCOnchainZaps.OnchainZapResolver -import com.vitorpamplona.amethyst.commons.model.observables.CreatedAtIdHexComparator -import com.vitorpamplona.amethyst.commons.model.observables.EventListMatchingFilter -import com.vitorpamplona.amethyst.commons.model.observables.NewEventMatchingFilter -import com.vitorpamplona.amethyst.commons.model.observables.NoteListMatchingFilter -import com.vitorpamplona.amethyst.commons.model.observables.Observable -import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList -import com.vitorpamplona.amethyst.commons.model.redirectStrayRelayGroupContent -import com.vitorpamplona.amethyst.commons.service.BundledInsert -import com.vitorpamplona.amethyst.commons.service.nwc.NwcPaymentTracker -import com.vitorpamplona.amethyst.commons.util.dateFormatter -import com.vitorpamplona.quartz.buzz.aeEngrams.EngramEvent -import com.vitorpamplona.quartz.buzz.agentProfiles.AgentProfileEvent -import com.vitorpamplona.quartz.buzz.amTurnMetrics.AgentTurnMetricEvent -import com.vitorpamplona.quartz.buzz.aoObserver.ObserverFrameEvent -import com.vitorpamplona.quartz.buzz.apPersonas.PersonaEvent -import com.vitorpamplona.quartz.buzz.audit.AuditEntryEvent -import com.vitorpamplona.quartz.buzz.cwChannelWindow.WindowBoundsEvent -import com.vitorpamplona.quartz.buzz.dm.DmAddMemberEvent -import com.vitorpamplona.quartz.buzz.dm.DmCreatedEvent -import com.vitorpamplona.quartz.buzz.dm.DmHideEvent -import com.vitorpamplona.quartz.buzz.dm.DmOpenEvent -import com.vitorpamplona.quartz.buzz.dvDmVisibility.DmVisibilityEvent -import com.vitorpamplona.quartz.buzz.erReminders.EventReminderEvent -import com.vitorpamplona.quartz.buzz.forum.ForumCommentEvent -import com.vitorpamplona.quartz.buzz.forum.ForumPostEvent -import com.vitorpamplona.quartz.buzz.forum.ForumVoteEvent -import com.vitorpamplona.quartz.buzz.huddles.HuddleEndedEvent -import com.vitorpamplona.quartz.buzz.huddles.HuddleGuidelinesEvent -import com.vitorpamplona.quartz.buzz.huddles.HuddleParticipantJoinedEvent -import com.vitorpamplona.quartz.buzz.huddles.HuddleParticipantLeftEvent -import com.vitorpamplona.quartz.buzz.huddles.HuddleReactionEvent -import com.vitorpamplona.quartz.buzz.huddles.HuddleStartedEvent -import com.vitorpamplona.quartz.buzz.iaIdentityArchival.ArchiveRequestEvent -import com.vitorpamplona.quartz.buzz.iaIdentityArchival.ArchivedIdentitiesListEvent -import com.vitorpamplona.quartz.buzz.iaIdentityArchival.ArchivedIdentityEvent -import com.vitorpamplona.quartz.buzz.iaIdentityArchival.UnarchiveRequestEvent -import com.vitorpamplona.quartz.buzz.iaIdentityArchival.UnarchivedIdentityEvent -import com.vitorpamplona.quartz.buzz.jobs.JobAcceptedEvent -import com.vitorpamplona.quartz.buzz.jobs.JobCancelEvent -import com.vitorpamplona.quartz.buzz.jobs.JobErrorEvent -import com.vitorpamplona.quartz.buzz.jobs.JobProgressEvent -import com.vitorpamplona.quartz.buzz.jobs.JobRequestEvent -import com.vitorpamplona.quartz.buzz.jobs.JobResultEvent -import com.vitorpamplona.quartz.buzz.managedAgents.ManagedAgentEvent -import com.vitorpamplona.quartz.buzz.moderation.ModerationBanEvent -import com.vitorpamplona.quartz.buzz.moderation.ModerationResolveReportEvent -import com.vitorpamplona.quartz.buzz.moderation.ModerationTimeoutEvent -import com.vitorpamplona.quartz.buzz.moderation.ModerationUntimeoutEvent -import com.vitorpamplona.quartz.buzz.moderation.ProductFeedbackEvent -import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent -import com.vitorpamplona.quartz.buzz.notifications.MemberRemovedNotificationEvent -import com.vitorpamplona.quartz.buzz.pairing.PairingEvent -import com.vitorpamplona.quartz.buzz.plPushLease.PushLeaseEvent -import com.vitorpamplona.quartz.buzz.presence.PresenceUpdateEvent -import com.vitorpamplona.quartz.buzz.presence.TypingIndicatorEvent -import com.vitorpamplona.quartz.buzz.relayAdmin.RelayAdminAddMemberEvent -import com.vitorpamplona.quartz.buzz.relayAdmin.RelayAdminChangeRoleEvent -import com.vitorpamplona.quartz.buzz.relayAdmin.RelayAdminRemoveMemberEvent -import com.vitorpamplona.quartz.buzz.stream.CanvasEvent -import com.vitorpamplona.quartz.buzz.stream.StreamMessageBookmarkedEvent -import com.vitorpamplona.quartz.buzz.stream.StreamMessageDiffEvent -import com.vitorpamplona.quartz.buzz.stream.StreamMessageEditEvent -import com.vitorpamplona.quartz.buzz.stream.StreamMessagePinnedEvent -import com.vitorpamplona.quartz.buzz.stream.StreamMessageScheduledEvent -import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event -import com.vitorpamplona.quartz.buzz.stream.StreamReminderEvent -import com.vitorpamplona.quartz.buzz.stream.SystemMessageEvent -import com.vitorpamplona.quartz.buzz.stream.SystemMessagePayload -import com.vitorpamplona.quartz.buzz.stream.sidecars.ChannelSummaryEvent -import com.vitorpamplona.quartz.buzz.stream.sidecars.PresenceSnapshotEvent -import com.vitorpamplona.quartz.buzz.teams.TeamEvent -import com.vitorpamplona.quartz.buzz.threading.buzzThreadReply -import com.vitorpamplona.quartz.buzz.workflow.ApprovalDenyEvent -import com.vitorpamplona.quartz.buzz.workflow.ApprovalGrantEvent -import com.vitorpamplona.quartz.buzz.workflow.WorkflowApprovalDeniedEvent -import com.vitorpamplona.quartz.buzz.workflow.WorkflowApprovalGrantedEvent -import com.vitorpamplona.quartz.buzz.workflow.WorkflowApprovalRequestedEvent -import com.vitorpamplona.quartz.buzz.workflow.WorkflowCancelledEvent -import com.vitorpamplona.quartz.buzz.workflow.WorkflowCompletedEvent -import com.vitorpamplona.quartz.buzz.workflow.WorkflowDefEvent -import com.vitorpamplona.quartz.buzz.workflow.WorkflowFailedEvent -import com.vitorpamplona.quartz.buzz.workflow.WorkflowStepCompletedEvent -import com.vitorpamplona.quartz.buzz.workflow.WorkflowStepFailedEvent -import com.vitorpamplona.quartz.buzz.workflow.WorkflowStepStartedEvent -import com.vitorpamplona.quartz.buzz.workflow.WorkflowTriggerEvent -import com.vitorpamplona.quartz.buzz.workflow.WorkflowTriggeredEvent -import com.vitorpamplona.quartz.buzz.wpWorkspaceProfile.SetWorkspaceProfileEvent -import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent -import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId -import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChatEditEvent -import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent -import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent -import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent -import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent -import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent -import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent -import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent -import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChatEvent -import com.vitorpamplona.quartz.experimental.citations.ExternalCitationEvent -import com.vitorpamplona.quartz.experimental.citations.HardcopyCitationEvent -import com.vitorpamplona.quartz.experimental.citations.PromptCitationEvent -import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent -import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent -import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId -import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent -import com.vitorpamplona.quartz.experimental.fitness.workout.ExerciseTemplateEvent -import com.vitorpamplona.quartz.experimental.fitness.workout.WorkoutRecordEvent -import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent -import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent -import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent -import com.vitorpamplona.quartz.experimental.library.BlossomPieceIndexEvent -import com.vitorpamplona.quartz.experimental.library.BookshelfDirectoryEvent -import com.vitorpamplona.quartz.experimental.library.LearningResourceEvent -import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent -import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent -import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent -import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent -import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent -import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent -import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent -import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent -import com.vitorpamplona.quartz.experimental.nns.NNSEvent -import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent -import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent -import com.vitorpamplona.quartz.experimental.ps1saves.Ps1SaveEvent -import com.vitorpamplona.quartz.experimental.publications.PublicationContentEvent -import com.vitorpamplona.quartz.experimental.publications.PublicationIndexEvent -import com.vitorpamplona.quartz.experimental.ratings.EntityRatingEvent -import com.vitorpamplona.quartz.experimental.ratings.RelayReviewEvent -import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent -import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent -import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent -import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent -import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent -import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent -import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent -import com.vitorpamplona.quartz.nip01Core.core.Address -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.isAddressable -import com.vitorpamplona.quartz.nip01Core.core.isRegular -import com.vitorpamplona.quartz.nip01Core.core.isReplaceable -import com.vitorpamplona.quartz.nip01Core.crypto.checkSignature -import com.vitorpamplona.quartz.nip01Core.crypto.verify -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.commands.toRelay.EventCmd -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterIndex -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag -import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag -import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents -import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionIndex -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 -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent -import com.vitorpamplona.quartz.nip18Reposts.BaseRepostEvent -import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent -import com.vitorpamplona.quartz.nip18Reposts.RepostEvent -import com.vitorpamplona.quartz.nip18Reposts.quotes.taggedQuoteIds -import com.vitorpamplona.quartz.nip19Bech32.isATag -import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip25Reactions.ExternalReactionEvent -import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelHideMessageEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMuteUserEvent -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent -import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip29RelayGroups.GroupId -import com.vitorpamplona.quartz.nip29RelayGroups.groupId -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupParticipantsEvent -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupPinnedEvent -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.SupportedRolesEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateGroupEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateInviteEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteEventEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteGroupEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.PutUserEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.RemoveUserEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.UpdatePinListEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.JoinRequestEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.LeaveRequestEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent -import com.vitorpamplona.quartz.nip32Labeling.LabelEvent -import com.vitorpamplona.quartz.nip34Git.grasp.UserGraspListEvent -import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent -import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent -import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestEvent -import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestUpdateEvent -import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent -import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent -import com.vitorpamplona.quartz.nip34Git.state.GitRepositoryStateEvent -import com.vitorpamplona.quartz.nip34Git.status.GitStatusEvent -import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent -import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent -import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent -import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent -import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent -import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent -import com.vitorpamplona.quartz.nip40Expiration.isExpired -import com.vitorpamplona.quartz.nip43RelayMembers.addMember.RelayAddMemberEvent -import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent -import com.vitorpamplona.quartz.nip43RelayMembers.removeMember.RelayRemoveMemberEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent -import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent -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.interestSet.InterestSetEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent -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.ProxyRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent -import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent -import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent -import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent -import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent -import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent -import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent -import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent -import com.vitorpamplona.quartz.nip53LiveActivities.nestsServers.NestsServersEvent -import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent -import com.vitorpamplona.quartz.nip53LiveActivities.raid.LiveActivitiesRaidEvent -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiMergeAcceptanceEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiMergeRequestEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiRedirectEvent -import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent -import com.vitorpamplona.quartz.nip57Zaps.validate.LnZapReceiptValidator -import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointCache -import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointResolver -import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlForm -import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent -import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent -import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent -import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent -import com.vitorpamplona.quartz.nip59Giftwrap.HasInnerEvent -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent -import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent -import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent -import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent -import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent -import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent -import com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent -import com.vitorpamplona.quartz.nip60Cashu.quote.CashuMintQuoteEvent -import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent -import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletEvent -import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent -import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent -import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.claimedSatsTotal -import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent -import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent -import com.vitorpamplona.quartz.nip64Chess.challenge.offer.LiveChessGameChallengeEvent -import com.vitorpamplona.quartz.nip64Chess.draw.LiveChessDrawOfferEvent -import com.vitorpamplona.quartz.nip64Chess.end.LiveChessGameEndEvent -import com.vitorpamplona.quartz.nip64Chess.game.ChessGameEvent -import com.vitorpamplona.quartz.nip64Chess.jester.JesterEvent -import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent -import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.RelayDiscoveryEvent -import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.RelayMonitorEvent -import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent -import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent -import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent -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.nip7DThreads.ThreadEvent -import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent -import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent -import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent -import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent -import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent -import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent -import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.dvmHeartbeat.DvmHeartbeatEvent -import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent -import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryRequest.NIP90UserDiscoveryRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryResponse.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.nipA4PublicMessages.PublicMessageEvent -import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallAnswerEvent -import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallHangupEvent -import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallIceCandidateEvent -import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent -import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRejectEvent -import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRenegotiateEvent -import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent -import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListEvent -import com.vitorpamplona.quartz.nipB1Bolt12Zaps.verify.Bolt12ZapValidation -import com.vitorpamplona.quartz.nipB1Bolt12Zaps.verify.Bolt12ZapValidator -import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent -import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent -import com.vitorpamplona.quartz.nipBCOnchainZaps.chain.OnchainBackend -import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent -import com.vitorpamplona.quartz.nipC0CodeSnippets.CodeSnippetEvent -import com.vitorpamplona.quartz.nipC7Chats.ChatEvent -import com.vitorpamplona.quartz.nipF4Podcasts.authored.AuthoredPodcastsEvent -import com.vitorpamplona.quartz.nipF4Podcasts.episode.PodcastEpisodeEvent -import com.vitorpamplona.quartz.nipF4Podcasts.favorites.FavoritePodcastsListEvent -import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent -import com.vitorpamplona.quartz.nipXXPodcasting20.episode.Podcasting20EpisodeEvent -import com.vitorpamplona.quartz.nipXXPodcasting20.trailer.Podcasting20TrailerEvent -import com.vitorpamplona.quartz.utils.Hex -import com.vitorpamplona.quartz.utils.Log -import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.cache.LargeCache -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.buffer -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.util.SortedSet - -object LocalCache : ILocalCache, ICacheProvider, Dao { - /** - * What this cache needs from the application shell around it: a scope to verify on, the - * NIP-95 blob directory, the relay identity and stats sinks, and the main-thread assertion. - * Defaults to [LocalCacheHost.Default], which supplies none of them; Android installs its - * own during app startup. - */ - @Volatile - var appHost: LocalCacheHost = LocalCacheHost - - val antiSpam = AntiSpamFilter { appHost } - - val users = LargeSoftCache() - val notes = LargeSoftCache() - val addressables = LargeSoftCache() - - val chatroomList = LargeCache() - val publicChatChannels = LargeCache() - val liveChatChannels = LargeCache() - val ephemeralChannels = LargeCache() - val geohashChannels = LargeCache() - val relayGroupChannels = LargeCache() - val concordChannels = LargeCache() - - val paymentTracker = NwcPaymentTracker() - - /** - * Bitcoin chain backend used by [consume]`(OnchainZapEvent)` to verify NIP-BC zaps - * against the actual on-chain transaction. `null` disables verification (incoming - * onchain zap events still get cached, but no `Note.zapsAmount` contribution). - * Set during account init. - */ - @Volatile - var onchainBackend: OnchainBackend? = null - - /** - * NIP-BC on-chain zap verification coordinator. Owns the chain-tip poller flow, - * the in-flight de-duplication of verifier calls across consume/reverify paths, - * and the parallelism cap. `consume(OnchainZapEvent)` delegates the async - * verification side here; the gallery's reverification driver also calls in here - * directly. - */ - val onchainZapResolver = OnchainZapResolver(this) - - /** - * NIP-B1 BOLT12 zap validator. Unlike onchain zaps, BOLT12 proof verification - * is synchronous (a self-contained `lnp` payer proof), so `consume(Bolt12ZapEvent)` - * validates inline and needs no async resolver. - */ - val bolt12ZapValidator = Bolt12ZapValidator() - - /** - * Resolver for LNURL provider metadata used by [consume]`(LnZapEvent)` to - * validate NIP-57 Appendix F. `null` skips the receipt-signer check (the - * receipt is still accepted on signature verification alone — matches legacy - * behavior); set this in app init so receipts can be verified against the - * recipient's provider's advertised `nostrPubkey`. - */ - @Volatile - var lnurlEndpointResolver: LnurlEndpointResolver? = null - - override val relayHints = HintIndexer() - - /** - * Cashu mint URL directory, populated passively as - * `NutzapInfoEvent` / `MintRecommendationEvent` / `CashuMintEvent` - * flow through `updateMintIndex`. Backs the autocomplete in any text - * field where the user types a mint URL. - * - * Use [mintDirectoryBackfilled] semantics via [ensureMintDirectoryBackfilled] - * when reading from the UI — the first call sweeps already-cached - * events so suggestions are useful before the next relay round-trip. - */ - val mintDirectory = MintDirectoryIndex() - - @Volatile private var mintDirectoryBackfilled = false - private val mintDirectoryBackfillLock = Any() - - /** - * Sweeps `notes` + `addressables` for any NIP-87 / NIP-61 event the - * cache already holds and feeds them into [mintDirectory]. The wallet - * directory + every other user's kind:10019 we've ever loaded may - * have arrived BEFORE [updateMintIndex] existed (or before any - * caller cared), so without this backfill the autocomplete is empty - * until new events arrive. - * - * Idempotent — subsequent calls return immediately. Best-effort: a - * scan failure is swallowed so the index stays usable even if the - * cache is in an unexpected state. - */ - fun ensureMintDirectoryBackfilled() { - if (mintDirectoryBackfilled) return - synchronized(mintDirectoryBackfillLock) { - if (mintDirectoryBackfilled) return - runCatching { - notes.forEach { _, note -> note.event?.let(::updateMintIndex) } - addressables.forEach { _, note -> note.event?.let(::updateMintIndex) } - } - mintDirectoryBackfilled = true - } - } - - val deletionIndex = DeletionIndex() - - /** - * Inverted index over the active [Observable]s. New events fan - * out only to observers whose filter actually narrows on a field - * the event carries (author, kind, single-letter tag), instead - * of waking every observer per event. Predicate-only observers - * (no underlying [Filter]) live in the unindexed pool and still - * see every event. - */ - val observables = FilterIndex() - - /** - * Memory-reclaim policy (soft-cache trims + hidden/old/expired/superseded event - * pruning) and the shared [CachePruner.unlinkAndRemove] removal primitive. - */ - val pruner = CachePruner(this) - - /** Prefix/content search over users, notes, and channels. */ - val search = CacheSearch(this) - - // The search entry points of ICacheProvider, so a shared state holder can ask a cache what it - // holds without naming this module. The policy stays in CacheSearch; these are the door. - - override fun findNotesMatching( - filters: List, - hidden: LiveHiddenUsers, - ) = search.findNotesMatching(filters, hidden) - - override fun findNotesStartingWith( - text: String, - hidden: LiveHiddenUsers, - ) = search.findNotesStartingWith(text, hidden) - - override fun findPublicChatChannelsStartingWith(text: String) = search.findPublicChatChannelsStartingWith(text) - - override fun findEphemeralChatChannelsStartingWith(text: String) = search.findEphemeralChatChannelsStartingWith(text) - - override fun findLiveActivityChannelsStartingWith(text: String) = search.findLiveActivityChannelsStartingWith(text) - - fun Filter.match(note: Note): Boolean { - val event = note.event - return if (event != null) { - match(event) - } else { - false - } - } - - fun filter(filter: Filter): SortedSet = filter(filter) { true } - - /** - * Every note matching [filter]'s NIP-01 fields that also satisfies [predicate]. - * - * [predicate] is where anything the wire type cannot express belongs — a NIP-50 `search` the - * matcher does not read, and viewer policy like the mute list, which a relay has no knowledge - * of and a `Filter` therefore has no field for. Keeping it a separate parameter is what lets - * search reuse this path instead of hand-rolling its own scan. - */ - fun filter( - filter: Filter, - predicate: (Note) -> Boolean, - ): SortedSet { - val byKinds = filter.kinds?.filter { it.isAddressable() || it.isReplaceable() } - - val addressableMatches = - if (!byKinds.isNullOrEmpty()) { - val byAuthors = filter.authors - if (!byAuthors.isNullOrEmpty()) { - // optimized - byKinds.flatMap { kind -> - byAuthors.flatMap { pubkey -> - addressables.filter(kind, pubkey) { _, note -> - filter.match(note) && predicate(note) - } - } - } - } else { - // optimized - byKinds.flatMap { kind -> - addressables.filter(kind) { _, note -> - filter.match(note) && predicate(note) - } - } - } - } else { - addressables.filter { _, note -> - filter.match(note) && predicate(note) - } - } - - val noteMatches = - notes.filter { _, note -> - val event = note.event - if (event != null && event.kind.isRegular()) { - filter.match(event) && predicate(note) - } else { - false - } - } - - val all = (addressableMatches + noteMatches).toSortedSet(CreatedAtIdHexComparator) - val limit = filter.limit ?: return all - - // Sorted first, then cut. Both halves arrive in hash-walk order, so taking before sorting - // dropped whichever matches the walk happened to reach last — the newest ones as often as - // not — and a query with 200 addressable matches never showed a single regular note. - if (all.size <= limit) return all - return all.asSequence().take(limit).toCollection(sortedSetOf(CreatedAtIdHexComparator)) - } - - fun observeNotes(filter: Filter): Flow> = - callbackFlow { - val newFilter = - NoteListMatchingFilter(filter, this@LocalCache::filter) { - trySend(it) - } - - newFilter.init() - - observables.register(filter, newFilter) - - awaitClose { - observables.unregister(newFilter) - } - }.buffer(kotlinx.coroutines.channels.Channel.CONFLATED) - - fun observeEvents(filter: Filter): Flow> = - callbackFlow { - val cachedFilter = - EventListMatchingFilter(filter, this@LocalCache::filter) { - trySend(it) - } - - cachedFilter.init() - - observables.register(filter, cachedFilter) - - awaitClose { - observables.unregister(cachedFilter) - } - }.buffer(kotlinx.coroutines.channels.Channel.CONFLATED) - - /** - * Emits each new event for which [predicate] returns true, one at a time, - * as it is inserted into the cache. Unlike [observeEvents], this does not - * accumulate a list — useful for per-event reactive pipelines like - * notifications. - * - * The predicate runs on every insertion, so keep it cheap. Callers with a - * Nostr [Filter] can pass `filter::match`; compose additional local checks - * (rolling windows, derived fields the Filter grammar can't express) with - * `&&`. - */ - fun observeNewEvents(predicate: (Event) -> Boolean): Flow = - callbackFlow { - val newFilter = - NewEventMatchingFilter(predicate) { - trySend(it) - } - - // Unindexed: predicate is opaque, the index can't narrow. - // Caller is delivered every event and runs the predicate. - observables.registerUnindexed(newFilter) - - awaitClose { - observables.unregister(newFilter) - } - } - - fun observeNewEvents(filter: Filter): Flow = - callbackFlow { - val newFilter = - NewEventMatchingFilter(filter::match) { - trySend(it) - } - - observables.register(filter, newFilter) - - awaitClose { - observables.unregister(newFilter) - } - } - - @Suppress("UNCHECKED_CAST") - fun observeLatestEvent(filter: Filter) = observeEvents(filter).map { it.firstOrNull() } - - fun observeLatestNote(filter: Filter) = observeNotes(filter).map { it.firstOrNull() } - - override fun checkGetOrCreateUser(key: String): User? = runCatching { getOrCreateUser(key) }.getOrNull() - - fun load(keys: List): List = keys.mapNotNull(::checkGetOrCreateUser) - - fun load(keys: Set): Set = keys.mapNotNullTo(mutableSetOf(), ::checkGetOrCreateUser) - - override fun getOrCreateUser(pubkey: HexKey): User { - require(isValidHex(key = pubkey)) { "$pubkey is not a valid hex" } - // Pass `this` as the UserContext — User now resolves each pinned - // addressable note (kind:10002 / 10050 / 10019) lazily on first - // read, instead of all-or-nothing at construction time. - return users.getOrCreate(pubkey) { User(it, userContext) } - } - - /** [UserContext] bridge to this cache's addressable lookup. */ - private val userContext = UserContext(::getOrCreateAddressableNoteInternal) - - override fun getUserIfExists(pubkey: String): User? { - if (pubkey.isEmpty()) return null - return users.get(pubkey) - } - - override fun countUsers(predicate: (String, User) -> Boolean): Int { - var count = 0 - users.forEach { key, user -> - if (predicate(key, user)) count++ - } - return count - } - - fun countContactLists(predicate: (ContactListEvent) -> Boolean): Int { - var count = 0 - addressables.filter(ContactListEvent.KIND).forEach { note -> - val event = note.event as? ContactListEvent - if (event != null && predicate(event)) count++ - } - return count - } - - fun getAddressableNoteIfExists(key: String): AddressableNote? = Address.parse(key)?.let { addressables.get(it) } - - fun getAddressableNoteIfExists(address: Address): AddressableNote? = addressables.get(address) - - override fun getNoteIfExists(hexKey: String): Note? = if (hexKey.length == 64) notes.get(hexKey) else Address.parse(hexKey)?.let { addressables.get(it) } - - fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId) - - fun getPublicChatChannelIfExists(key: String): PublicChatChannel? = publicChatChannels.get(key) - - fun getEphemeralChatChannelIfExists(key: RoomId): EphemeralChatChannel? = ephemeralChannels.get(key) - - fun getGeohashChannelIfExists(geohash: String): GeohashChatChannel? = geohashChannels.get(geohash) - - fun getRelayGroupChannelIfExists(key: GroupId): RelayGroupChannel? = relayGroupChannels.get(key) - - /** Every relay group we know of that is hosted on [relay] (its channel directory). */ - override fun getRelayGroupChannelsOnRelay(relay: NormalizedRelayUrl): List = relayGroupChannels.filter { key, _ -> key.relayUrl == relay } - - override fun allRelayGroupChannels(): List = relayGroupChannels.values().toList() - - /** - * The [RelayGroupChannel] a group-scoped content [note] belongs to, resolved the same way - * [attachToRelayGroupIfScoped] keyed it: the serving-relay key first (fast O(1)), then the single - * channel bearing this group id when the note has no usable provenance relay. Read-only — used by - * feed filters that need a note's channel without scanning every channel's timeline. - */ - fun getRelayGroupChannelForContent(note: Note): RelayGroupChannel? { - val groupId = note.event?.groupId() ?: return null - note.relays.firstNotNullOfOrNull { getRelayGroupChannelIfExists(GroupId(groupId, it)) }?.let { return it } - return relayGroupChannels.filter { key, _ -> key.id == groupId }.singleOrNull() - } - - /** - * Every host relay of the NIP-29 group [event] is scoped to (its `h` tag), or an empty set when the - * event carries no group scope or the group is unknown to this cache. - * - * Keyed by group id alone rather than by [GroupId]: an event about to be *sent* (a reaction, a zap - * request, a comment) knows which room it belongs to but not which relay hosts it — that is exactly - * what this resolves. Group ids are relay-minted UUIDs, so the same id on two hosts is a - * theoretical case, and answering with both is the safe reading of it: the content reaches every - * host that claims the room, and none that don't. - */ - fun relayGroupHostsFor(event: Event): Set { - val groupId = event.groupId() ?: return emptySet() - return relayGroupChannels.filter { key, _ -> key.id == groupId }.mapTo(mutableSetOf()) { it.groupId.relayUrl } - } - - fun getLiveActivityChannelIfExists(key: Address): LiveActivitiesChannel? = liveChatChannels.get(key) - - fun getNoteIfExists(event: Event): Note? = - if (event is AddressableEvent) { - getAddressableNoteIfExists(event.addressTag()) - } else { - getNoteIfExists(event.id) - } - - fun getOrCreateNote(event: Event): Note = - if (event is AddressableEvent) { - getOrCreateAddressableNote(event.address()) - } else { - getOrCreateNote(event.id) - } - - fun checkGetOrCreateNote(etag: ETag): Note? { - if (isValidHex(etag.eventId)) { - return getOrCreateNote(etag) - } - return null - } - - override fun checkGetOrCreateNote(hexKey: String): Note? { - if (ATag.isATag(hexKey)) { - return checkGetOrCreateAddressableNote(hexKey) - } - if (isValidHex(hexKey)) { - val note = getOrCreateNote(hexKey) - val noteEvent = note.event - return if (noteEvent is AddressableEvent) { - // upgrade to the latest - val newNote = getOrCreateAddressableNote(noteEvent.address()) - - if (newNote.event == null) { - val author = note.author ?: getOrCreateUser(noteEvent.pubKey) - newNote.loadEvent(noteEvent as Event, author, emptyList()) - note.moveAllReferencesTo(newNote) - } - - newNote - } else { - note - } - } - return null - } - - override fun getEventStream(): com.vitorpamplona.amethyst.commons.model.cache.ICacheEventStream = - object : com.vitorpamplona.amethyst.commons.model.cache.ICacheEventStream { - override val newEventBundles = live.newEventBundles - override val deletedEventBundles = live.deletedEventBundles - } - - override fun hasBeenDeleted(event: Any): Boolean = - if (event is Event) { - deletionIndex.hasBeenDeleted(event) - } else { - false - } - - /** - * Checks if a kind-5 event from the addressable's own author has deleted this - * address. Works for empty addressable shells whose event is not loaded yet. - */ - fun hasBeenDeleted(address: Address): Boolean = deletionIndex.hasBeenDeleted(address, address.pubKeyHex) - - fun getOrAddAliasNote( - idHex: String, - note: Note, - ): Note { - require(isValidHex(idHex)) { "$idHex is not a valid hex" } - - return notes.getOrCreate(idHex) { - note - } - } - - override fun getOrCreateNote(hex: String): Note { - require(isValidHex(hex)) { "$hex is not a valid hex" } - - return notes.getOrCreate(hex) { - Note(hex) - } - } - - fun getOrCreateChatroomList(key: HexKey): ChatroomList = chatroomList.getOrCreate(key) { ChatroomList(key) } - - fun getOrCreatePublicChatChannel(key: HexKey): PublicChatChannel = publicChatChannels.getOrCreate(key) { PublicChatChannel(key) } - - fun getOrCreateLiveChannel(key: Address): LiveActivitiesChannel = liveChatChannels.getOrCreate(key) { LiveActivitiesChannel(key) } - - fun getOrCreateEphemeralChannel(key: RoomId): EphemeralChatChannel = ephemeralChannels.getOrCreate(key) { EphemeralChatChannel(key) } - - fun getOrCreateGeohashChannel(geohash: String): GeohashChatChannel = geohashChannels.getOrCreate(geohash) { GeohashChatChannel(geohash) } - - fun getOrCreateRelayGroupChannel(key: GroupId): RelayGroupChannel = relayGroupChannels.getOrCreate(key) { RelayGroupChannel(key) } - - fun getConcordChannelIfExists(key: ConcordChannelId): ConcordChannel? = concordChannels.get(key) - - fun getOrCreateConcordChannel(key: ConcordChannelId): ConcordChannel = concordChannels.getOrCreate(key) { ConcordChannel(key) } - - /** - * Any known channel of Concord community [communityId], or null when we hold none. - * - * A community is not itself a cached object — it has no metadata event, only a folded Control - * Plane — so its display fields (`communityName`, icon, relays) are carried on every one of its - * channels. Callers that need to *name* a community therefore ask for whichever channel we have. - */ - fun getAnyConcordChannelOfCommunity(communityId: HexKey): ConcordChannel? = concordChannels.filter { key, _ -> key.communityId == communityId }.firstOrNull() - - /** - * Lands a decrypted Concord chat rumor in the cache as a real Note and, for - * message-like kinds, attaches it to its channel so the shared chat feed and - * the Messages inbox render it (with previews, threading, OTS, reactions/zaps - * reusing the same id-keyed machinery as every other chat). Reactions (kind 7), - * deletes (kind 5), etc. are consumed too — they wire to their target Note by - * `e`-tag through [justConsume] — but are not themselves added as channel rows. - * - * Fed by [com.vitorpamplona.amethyst.commons.model.concord.ConcordSessionManager] - * once a wrap decrypts + validates against the folded Control Plane. - */ - fun consumeConcordRumor( - communityId: String, - channelIdHex: String, - rumor: Event, - seenOnRelays: Set = emptySet(), - ) { - // Attach to the channel BEFORE justConsume sets the event and notifies feeds, - // so the note already carries its ConcordChannel gatherer when it flows through - // the Messages-list incremental filter (which routes rows by that gatherer). - val messageRow = - if (rumor is ChatEvent || rumor is CommentEvent) { - val ch = getOrCreateConcordChannel(ConcordChannelId(communityId, channelIdHex)) - val note = getOrCreateNote(rumor.id) - // Skip attaching a row for a message we already know is deleted (its kind-5 delete - // was processed first). Otherwise every reproject — which re-emits the whole wrap - // buffer — would re-add then re-remove it, churning the feed. justConsume still - // records the (already-known) deletion below; a delete arriving LATER is handled by - // the normal deletion cascade unlinking the note from its gatherers. - if (!deletionIndex.hasBeenDeleted(rumor)) ch.addNote(note) - ch to note - } else { - null - } - // wasVerified = true: a Concord rumor is unsigned (its `sig` is empty), so a signature - // check would fail and the event would never load onto its Note — leaving the chat row - // stuck on the "loading / not found" placeholder. Its authenticity is already established - // by the envelope open path (ConcordStreamEnvelope.open verifies the seal signature, - // binds rumor.pubKey == seal.pubKey, and checks rumor.verifyId()), exactly like a NIP-59 - // gift-wrapped DM rumor, so we consume it as pre-verified. - justConsume(rumor, null, true) - - // A Concord rumor is decrypted locally with the plane key, so it arrives with no - // per-relay attribution (relay = null above). Stamp the relays its carrying wrap was - // seen on — mirroring NIP-17's addRelayToNoteAndInners — so the chat UI can show where - // the message actually came from, not just the channel's configured relays. - if (seenOnRelays.isNotEmpty()) { - getNoteIfExists(rumor.id)?.takeIf { it.event != null }?.let { note -> - seenOnRelays.forEach { note.addRelay(it) } - } - } - - // justConsume bails without loading the event when the rumor has already been deleted - // (a kind-5 delete referencing it was processed first — easy to hit in Concord because a - // reproject re-emits the whole wrap buffer and ordering isn't guaranteed) or fails to - // verify. We attached the row up front, so an unpopulated note would otherwise linger as a - // permanent "Event is loading…" ghost. Drop it; the reverse order (delete after the message) - // is already handled by the normal deletion cascade unlinking the note from its gatherers. - messageRow?.let { (ch, note) -> - if (note.event == null) { - ch.removeNote(note) - } else { - // The row was attached (addNote) BEFORE justConsume set the event, so addNote saw a - // null createdAt and could not pick lastNote or order the feed. The event is loaded - // now — refresh so the channel's last-message preview, unread count, and ordering are - // correct (otherwise lastNote stays null forever and every row reads "No messages yet"). - ch.refreshAfterEventLoad(note) - } - } - } - - fun checkGetOrCreatePublicChatChannel(key: String): PublicChatChannel? { - if (isValidHex(key)) { - return getOrCreatePublicChatChannel(key) - } - return null - } - - private fun isValidHex(key: String): Boolean { - if (key.length != 64) return false - return Hex.isHex64(key) - } - - override fun checkGetOrCreateAddressableNote(key: String): AddressableNote? = - try { - val addr = Address.parse(key) - if (addr != null) { - getOrCreateAddressableNote(addr) - } else { - null - } - } catch (e: IllegalArgumentException) { - Log.e("LocalCache", "Invalid Key to create channel: $key", e) - null - } - - fun getOrCreateAddressableNoteInternal(key: Address): AddressableNote = addressables.getOrCreate(key) { AddressableNote(key) } - - override fun getOrCreateAddressableNote(address: Address): AddressableNote { - val note = getOrCreateAddressableNoteInternal(address) - // Loads the user outside a Syncronized block to avoid blocking - if (note.author == null) { - note.author = checkGetOrCreateUser(address.pubKeyHex) - } - return note - } - - fun getOrCreateNote(key: GenericETag): Note { - val note = getOrCreateNote(key.eventId) - // Loads the user outside a Syncronized block to avoid blocking - val possibleAuthor = key.author - if (note.author == null && possibleAuthor != null) { - note.author = checkGetOrCreateUser(possibleAuthor) - } - val relayHint = key.relay - if (relayHint != null) { - note.addRelay(relayHint) - } - return note - } - - fun consume( - event: MetadataEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - // new event - consumeBaseReplaceable(event, relay, wasVerified) - - val user = getOrCreateUser(event.pubKey) - - if (user.metadata().shouldUpdateWith(event)) { - val newUserMetadata = event.contactMetaData() - if (newUserMetadata != null && (wasVerified || justVerify(event))) { - user.updateUserInfo(newUserMetadata, event) - if (relay != null) { - user.addRelayBeingUsed(relay, event.createdAt) - } - - return true - } - } - - return false - } - - fun consumeRegularEvent( - event: Event, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val note = getOrCreateNote(event.id) - val author = getOrCreateUser(event.pubKey) - - if (relay != null) { - author.addRelayBeingUsed(relay, event.createdAt) - // A gift wrap re-delivered by another relay is a duplicate (returns - // false below and is never re-processed), so drill into the already - // unwrapped chain here — otherwise the relay never reaches the - // rumor note that the chat UI actually renders. - addRelayToNoteAndInners(note, relay) - } - - // Already processed this event. - if (note.event != null) return false - - if (event is BaseNoteEvent && antiSpam.isSpam(event, relay)) { - return false - } - - return if (wasVerified || justVerify(event)) { - val replyTo = computeReplyTo(event) - - note.loadEvent(event, author, replyTo) - - // Counts the replies - replyTo.forEach { it.addReply(note) } - - // NIP-18 quote reposts: a note carrying a `q` tag is a quote-repost of the - // quoted note. Count it as a boost so it shows in the quoted note's repost - // counter alongside kind:6/kind:16 reposts. The quoted note is deliberately - // kept out of `replyTo` so the quote still renders as a root post in the home - // feed (see Note.isNewThread); deletion cleanup lives in CachePruner.unlinkAndRemove. - addQuoteBoosts(event, note, replyTo) - - refreshNewNoteObservers(note) - - true - } else { - false - } - } - - /** - * Adds [note] as a boost of every event/address referenced by a NIP-18 `q` tag - * (a quote-repost). Targets already in [replyTo] are skipped so a note that both - * replies to and quotes the same note isn't counted twice, and self-quotes are - * ignored. - */ - private fun addQuoteBoosts( - event: Event, - note: Note, - replyTo: List, - ) { - event.taggedQuoteIds().forEach { quotedId -> - val quoted = checkGetOrCreateNote(quotedId) - if (quoted != null && quoted != note && quoted !in replyTo) { - quoted.addBoost(note) - } - } - } - - fun consume( - event: NipTextEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - val isVerified = - if (version.event == null && (wasVerified || justVerify(event))) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - true - } else { - wasVerified - } - - if (relay != null) { - author.addRelayBeingUsed(relay, event.createdAt) - note.addRelay(relay) - } - - // Already processed this event. - if (note.event?.id == event.id) return wasVerified - - if (antiSpam.isSpam(event, relay)) { - return false - } - - if (isVerified || justVerify(event)) { - val replyTo = computeReplyTo(event) - - if (event.createdAt > (note.createdAt() ?: 0L)) { - note.loadEvent(event, author, replyTo) - - refreshNewNoteObservers(note) - - return true - } - } - - return false - } - - fun consume( - event: LongTextNoteEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - val isVerified = - if (version.event == null && (wasVerified || justVerify(event))) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - true - } else { - wasVerified - } - - if (relay != null) { - author.addRelayBeingUsed(relay, event.createdAt) - note.addRelay(relay) - } - - // Already processed this event. - if (note.event?.id == event.id) return wasVerified - - if (antiSpam.isSpam(event, relay)) { - return false - } - - if (isVerified || justVerify(event)) { - val replyTo = computeReplyTo(event) - - if (event.createdAt > (note.createdAt() ?: 0L)) { - note.loadEvent(event, author, replyTo) - - refreshNewNoteObservers(note) - - return true - } - } - - return false - } - - fun consume( - event: PaymentTargetsEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - val isVerified = - if (version.event == null && (wasVerified || justVerify(event))) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - true - } else { - wasVerified - } - - if (relay != null) { - author.addRelayBeingUsed(relay, event.createdAt) - note.addRelay(relay) - } - - // Already processed this event. - if (note.event?.id == event.id) return wasVerified - - if (antiSpam.isSpam(event, relay)) { - return false - } - - if ((isVerified || justVerify(event)) && event.createdAt > (note.createdAt() ?: 0L)) { - val replyTo = computeReplyTo(event) - - note.loadEvent(event, author, replyTo) - - refreshNewNoteObservers(note) - - return true - } - - return false - } - - fun consume( - event: WikiNoteEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - val isVerified = - if (version.event == null && (wasVerified || justVerify(event))) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - true - } else { - wasVerified - } - - if (relay != null) { - author.addRelayBeingUsed(relay, event.createdAt) - note.addRelay(relay) - } - - // Already processed this event. - if (note.event?.id == event.id) return wasVerified - - if (antiSpam.isSpam(event, relay)) { - return false - } - - if ((isVerified || justVerify(event)) && event.createdAt > (note.createdAt() ?: 0L)) { - val replyTo = computeReplyTo(event) - - note.loadEvent(event, author, replyTo) - - refreshNewNoteObservers(note) - - return true - } - - return false - } - - @Suppress("DEPRECATION") - fun computeReplyTo(event: Event): List = - when (event) { - is ZapPollEvent -> { - event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } - } - - is LongTextNoteEvent -> { - event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } - } - - is GitReplyEvent -> { - event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.mapNotNull { checkGetOrCreateNote(it) } - } - - is GitPullRequestUpdateEvent -> { - // Link the update to its parent PR so it lands in the PR's - // replies collection (and picks up its target for threading). - // The repository ATag isn't a reply target — skip it. - listOfNotNull(event.parentPullRequestId()?.let { checkGetOrCreateNote(it) }) - } - - is GitStatusEvent -> { - // A status event roots itself at a patch/PR/issue via a - // marked-`root` `e` tag; link only that so the transition - // appears in the target's replies (GitStatusIndex reduces the - // observed stream separately and doesn't need this wiring, but - // ThreadFeedView and the notifications-tab reply chain do). - listOfNotNull(event.rootEventId()?.let { checkGetOrCreateNote(it) }) - } - - is TextNoteEvent -> { - event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } - } - - is CommentEvent -> { - event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } - } - - is StreamMessageV2Event -> { - // A Buzz thread reply links to the message it answers (its `reply`-marked e-tag) so it - // lands in that message's replies (the minichat). A plain message / non-reply has no marker. - listOfNotNull(event.tags.buzzThreadReply()?.let { checkGetOrCreateNote(it) }) - } - - is VoiceReplyEvent -> { - event.markedReplyTos().mapNotNull { checkGetOrCreateNote(it) } - } - - is ChatMessageEvent -> { - event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } - } - - is ChatMessageEncryptedFileHeaderEvent -> { - event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } - } - - is LnZapEvent -> { - event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().map { getOrCreateAddressableNote(it) } + - (event.zapRequest?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptyList()) - } - - is OnchainZapEvent -> { - // NIP-BC zaps can target an event id (e), an addressable event (a), - // or just the recipient profile (p). Profile-only zaps have no - // Note target and are surfaced through profile zap queries. - buildList { - event.zappedEvent()?.let { checkGetOrCreateNote(it)?.let { add(it) } } - event.zappedAddress()?.let { coord -> - Address.parse(coord)?.let { add(getOrCreateAddressableNote(it)) } - } - } - } - - is Bolt12ZapEvent -> { - // NIP-B1 BOLT12 zaps target an event (e), an addressable event (a), - // or just the recipient profile (p) — same shape as onchain zaps. - buildList { - event.zappedEvent()?.let { checkGetOrCreateNote(it)?.let { add(it) } } - event.zappedAddress()?.let { coord -> - Address.parse(coord)?.let { add(getOrCreateAddressableNote(it)) } - } - } - } - - is NutzapEvent -> { - // The zapped event is carried in the kind:9321's `e` tags - // (and optionally an `a` tag for addressables). Whichever - // notes those resolve to receive the nutzap entry — - // analogous to how LnZapEvent flows into `addZap`. - event.linkedEventIds().mapNotNull { checkGetOrCreateNote(it) } - } - - is LnZapRequestEvent -> { - event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().map { getOrCreateAddressableNote(it) } - } - - is AcceptedBadgeSetEvent, is ProfileBadgesEvent -> { - event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().map { getOrCreateAddressableNote(it) } - } - - is BadgeAwardEvent -> { - event.awardDefinition().map { getOrCreateAddressableNote(it) } - } - - is PrivateDmEvent -> { - event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } - } - - is RepostEvent, is GenericRepostEvent -> { - val repost = event as BaseRepostEvent - listOfNotNull( - repost.boostedEventId()?.let { checkGetOrCreateNote(it) }, - repost.boostedAddress()?.let { getOrCreateAddressableNote(it) }, - ) - } - - is CommunityPostApprovalEvent -> { - event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) } + - event.approvedAddresses().map { getOrCreateAddressableNote(it) } - } - - is ReactionEvent -> { - event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().map { getOrCreateAddressableNote(it) } - } - - is WakeUpEvent -> { - // Link the referenced events so filterMissingEvents will query - // for them when the WakeUp note is in an EventFinder subscription. - event.eventIds().mapNotNull { checkGetOrCreateNote(it) } - } - - is ChannelMessageEvent -> { - event.tagsWithoutCitations().filter { it != event.channelId() }.mapNotNull { checkGetOrCreateNote(it) } - } - - is LiveActivitiesChatMessageEvent -> { - event.tagsWithoutCitations().filter { it != event.activity()?.toTag() }.mapNotNull { checkGetOrCreateNote(it) } - } - - is TorrentCommentEvent -> { - event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } - } - - is ChatEvent -> { - // Amethyst's own kind:9 replies carry the parent as a NIP-18 - // `q` tag (see ChatEvent.replyingTo), but the broader Marmot - // ecosystem — WhiteNoise in particular — threads kind:9 chats - // with a plain NIP-10 `e` tag. Accept both so inbound replies - // from either client show their quote bubble in the feed. - val eTagTargets = - event.tags - .filter { it.size > 1 && it[0] == "e" } - .map { it[1] } - val qTagTargets = event.quotedEvents().map { it.eventId } - (eTagTargets + qTagTargets).mapNotNull { checkGetOrCreateNote(it) } - } - - else -> { - emptyList() - } - } - - private fun consume( - event: LiveActivitiesEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - val isVerified = - if (version.event == null && (wasVerified || justVerify(event))) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - true - } else { - wasVerified - } - - if (note.event?.id == event.id) return false - - if (event.createdAt > (note.createdAt() ?: 0L) && (isVerified || justVerify(event))) { - note.loadEvent(event, author, emptyList()) - - val channel = getOrCreateLiveChannel(note.address) - - if (relay != null) { - channel.addRelay(relay) - } - - val creator = event.host()?.let { checkGetOrCreateUser(it.pubKey) } ?: author - - channel.updateChannelInfo(creator, event, note) - - refreshNewNoteObservers(note) - - return true - } - - return false - } - - fun consume( - event: StatusEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val author = getOrCreateUser(event.pubKey) - val note = event.toAddressableNote() - val new = consumeBaseReplaceable(event, relay, wasVerified) - - if (new) { - author.statusState().addStatus(note) - } - - return new - } - - fun Event.toNote() = getOrCreateNote(id) - - fun AddressableEvent.toAddressableNote() = getOrCreateAddressableNote(address()) - - fun consume( - event: ContactCardEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val note = event.toAddressableNote() - val new = consumeBaseReplaceable(event, relay, wasVerified) - - if (new) { - val about = checkGetOrCreateUser(event.aboutUser()) ?: return new - about.cards().addCard(note) - } - - return new - } - - fun consume( - event: OtsEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val version = getOrCreateNote(event.id) - val author = getOrCreateUser(event.pubKey) - - // Already processed this event. - if (version.event != null) return false - - if (wasVerified || justVerify(event)) { - version.loadEvent(event, author, emptyList()) - - // Anchor the attestation to the note it timestamps (like an edit to its message), so it - // survives exactly as long as that note and is dropped when the note is deleted or pruned. - // addTimestamp invalidates the target's `ots` flow so the OTS pill re-derives — the old - // code invalidated the attestation's OWN (observer-less) flow, so the target never updated. - event.digestEventId()?.let { targetId -> - getOrCreateNote(targetId).addTimestamp(version) - } - - refreshNewNoteObservers(version) - return true - } - - return false - } - - private fun consumeBaseReplaceable( - event: Event, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - // TODO: Redo the Event sctructure in Quartz to avoid this check - check(event is AddressableEvent) { "Event must be addressable: ${event.kind}" } - - val version = getOrCreateNote(event.id) - val replaceableNote = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - val isVerified = - if (version.event == null && (wasVerified || justVerify(event))) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(replaceableNote) - true - } else { - wasVerified - } - - if (relay != null) { - author.addRelayBeingUsed(relay, event.createdAt) - replaceableNote.addRelay(relay) - } - - // Already processed this event. - if (replaceableNote.event?.id == event.id) return isVerified - - return if (event.createdAt > (replaceableNote.createdAt() ?: 0L) && (isVerified || justVerify(event))) { - // clear index from previous tags - replaceableNote.replyTo?.forEach { - it.removeNote(replaceableNote) - } - - replaceableNote.loadEvent(event, author, computeReplyTo(event)) - - refreshNewNoteObservers(replaceableNote) - - true - } else { - false - } - } - - fun consume( - event: DeletionEvent, - 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 - - return 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() ?: 0L) <= 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) - } - } - - 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() ?: 0L) <= event.createdAt) { - // Counts the replies - deleteNote(deleteNote) - - addressables.remove(deleteNote.address) - } - } - - notes.forEach { _, note -> - val noteEvent = note.event - if (noteEvent is AddressableEvent && - noteEvent.addressTag() in addressSet && - noteEvent.pubKey == event.pubKey && - noteEvent.createdAt <= event.createdAt - ) { - deleteNote(note) - } - } - } - - refreshNewNoteObservers(note) - - true - } else { - false - } - } - - override 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) } - is GeohashChatEvent -> noteEvent.geohash()?.let { getGeohashChannelIfExists(it) } - else -> null - } - - /** - * NIP-09 delete of a single targeted event. - * - * Removal has two halves: unlinking the note from everything that points AT it - * (its parents, channels, and the per-user report/card/status/poll indexes — - * all handled by [CachePruner.unlinkAndRemove]); and dealing with the note's OWN children - * (the notes that point at IT). The delete path and the prune path share the - * first half and differ only on the second: - * - delete (here): the children are independent events and stay in the cache; - * [Note.detachFromChildren] only severs their back-reference so the removed - * shell can neither leak (held alive by a child's `replyTo`) nor be later - * resurrected by `computeReplyTo` as a second Note for the same id. - * - prune (see [CachePruner.unlinkAndRemove] callers): the whole child subtree is removed. - * - * Rumors additionally drop the envelope notes that delivered them. - */ - private fun deleteNote(deleteNote: Note) { - deleteEnvelopes(deleteNote) - - deleteNote.detachFromChildren() - - pruner.unlinkAndRemove(deleteNote) - } - - /** - * Removes the envelope notes that delivered [rumorNote]'s rumor: its - * host (normally the kind-1059 wrap; a bare kind-13 seal otherwise) - * and, when the host is a wrap, the seal layer it carried. Public - * events have no envelopes and are ignored. - */ - fun deleteEnvelopes(rumorNote: Note) { - val host = rumorNote.rumorHost ?: return - - getNoteIfExists(host.id)?.let { hostNote -> - (hostNote.event as? GiftWrapEvent)?.innerEventId?.let { sealId -> - getNoteIfExists(sealId)?.let { sealNote -> - sealNote.clearFlow() - refreshDeletedNoteObservers(sealNote) - } - notes.remove(sealId) - } - hostNote.clearFlow() - refreshDeletedNoteObservers(hostNote) - } - - notes.remove(host.id) - rumorNote.rumorHost = null - } - - fun consume( - event: RepostEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val note = getOrCreateNote(event.id) - - if (relay != null) { - getOrCreateUser(event.pubKey).addRelayBeingUsed(relay, event.createdAt) - note.addRelay(relay) - } - - // Already processed this event. - if (note.event != null) return false - - if (wasVerified || justVerify(event)) { - val author = getOrCreateUser(event.pubKey) - val repliesTo = computeReplyTo(event) - - note.loadEvent(event, author, repliesTo) - - // Counts the replies - repliesTo.forEach { it.addBoost(note) } - - event.containedPost()?.let { - checkDeletionAndConsume(it, relay, false) - } - - refreshNewNoteObservers(note) - - return true - } - return false - } - - fun consume( - event: GenericRepostEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val note = getOrCreateNote(event.id) - - if (relay != null) { - getOrCreateUser(event.pubKey).addRelayBeingUsed(relay, event.createdAt) - note.addRelay(relay) - } - - // Already processed this event. - if (note.event != null) return false - - if (wasVerified || justVerify(event)) { - val author = getOrCreateUser(event.pubKey) - val repliesTo = computeReplyTo(event) - - note.loadEvent(event, author, repliesTo) - - // Counts the replies - repliesTo.forEach { it.addBoost(note) } - - event.containedPost()?.let { - checkDeletionAndConsume(it, relay, false) - } - - refreshNewNoteObservers(note) - - return true - } - - return false - } - - fun consume( - event: CommunityPostApprovalEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val note = getOrCreateNote(event.id) - - // Approval notes render their own relay list directly in community feeds - // (there is no repost-style indirection to a replyTo for them), so without - // this attribution the "accepted by relays" gallery line stays empty forever. - if (relay != null) { - getOrCreateUser(event.pubKey).addRelayBeingUsed(relay, event.createdAt) - note.addRelay(relay) - } - - // Already processed this event. - if (note.event != null) return false - - if (wasVerified || justVerify(event)) { - val author = getOrCreateUser(event.pubKey) - - val communities = event.communityAddresses() - val eventsApproved = computeReplyTo(event) - - val repliesTo = communities.map { getOrCreateAddressableNote(it) } - - note.loadEvent(event, author, eventsApproved) - - // Counts the replies - repliesTo.forEach { it.addBoost(note) } - - eventsApproved.forEach { it.addBoost(note) } - - event.containedPost()?.let { - checkDeletionAndConsume(it, relay, false) - } - - refreshNewNoteObservers(note) - - return true - } - - return false - } - - fun consume( - event: ReactionEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val note = getOrCreateNote(event.id) - - // Already processed this event. - if (note.event != null) return true - - if (wasVerified || justVerify(event)) { - val author = getOrCreateUser(event.pubKey) - val repliesTo = computeReplyTo(event) - - note.loadEvent(event, author, repliesTo) - - repliesTo.forEach { it.addReaction(note) } - - refreshNewNoteObservers(note) - - return true - } - - return false - } - - fun consume( - event: LabelEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val note = getOrCreateNote(event.id) - - // Already processed this event. - if (note.event != null) return true - - if (wasVerified || justVerify(event)) { - val author = getOrCreateUser(event.pubKey) - val repliesTo = computeReplyTo(event) - - note.loadEvent(event, author, repliesTo) - - // Attach NIP-32 hashtag labels (namespace #t) to the labeled events so the - // hashtag feed can surface posts tagged by a follow and attribute the labeler. - val hashtags = event.hashtagAssociations() - if (hashtags.isNotEmpty()) { - event.labeledEvents().mapNotNull { checkGetOrCreateNote(it) }.forEach { target -> - hashtags.forEach { hashtag -> target.addLabel(hashtag, note) } - - // If the labeled post is already in cache, re-notify feed observers so the - // hashtag feed can pick it up now that a (possibly followed) user labeled it. - if (target.event != null) refreshNewNoteObservers(target) - } - } - - refreshNewNoteObservers(note) - - return true - } - - return false - } - - fun consume( - event: ReportEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val note = getOrCreateNote(event.id) - - val new = consumeRegularEvent(event, relay, wasVerified) - - if (new) { - val authorsReported = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.pubKey) } - val eventsReported = - event.reportedPost().mapNotNull { checkGetOrCreateNote(it.eventId) } + - event.reportedAddresses().map { getOrCreateAddressableNote(it.address) } - - // Every report that names an author also lands in the naming index, including those - // filed against a note rather than a profile. Read only by the DM sender warning — the - // hide threshold keeps counting `receivedReportsByAuthor` alone. - if (eventsReported.isEmpty()) { - authorsReported.forEach { author -> - val reports = author.reports() - reports.addReport(note) - reports.addReportNamingUser(note) - } - } else { - eventsReported.forEach { it.addReport(note) } - - // Index only the authors whose own `p` tag carries a report type: an event-scoped - // report can `p`-tag an incidentally-mentioned third party with no type of its own, - // and there is no threshold here to absorb that noise the way - // `receivedReportsByAuthor`'s hide path does. - val explicitlyTyped = event.reportedAuthorsWithOwnType().mapTo(mutableSetOf()) { it.pubKey } - authorsReported.forEach { author -> - if (author.pubkeyHex in explicitlyTyped) author.reports().addReportNamingUser(note) - } - } - } - - return new - } - - fun consume( - event: ChannelCreateEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - // Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") - val oldChannel = getOrCreatePublicChatChannel(event.id) - val author = getOrCreateUser(event.pubKey) - val note = getOrCreateNote(event.id) - - val isVerified = - if (note.event == null && (wasVerified || justVerify(event))) { - oldChannel.addNote(note, relay) - note.loadEvent(event, author, emptyList()) - - refreshNewNoteObservers(note) - true - } else { - wasVerified - } - - if (event.createdAt <= oldChannel.updatedMetadataAt) { - return false // older data, does nothing - } - - if ((oldChannel.creator == null || oldChannel.creator == author) && (isVerified || justVerify(event))) { - oldChannel.updateChannelInfo(author, event, note) - } - - return isVerified - } - - fun consume( - event: ChannelMetadataEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val channelId = event.channelId() - if (channelId.isNullOrBlank()) return false - - // new event - val oldChannel = checkGetOrCreatePublicChatChannel(channelId) ?: return false - - val author = getOrCreateUser(event.pubKey) - val note = getOrCreateNote(event.id) - - val isVerified = - if (event.createdAt > oldChannel.updatedMetadataAt) { - if (wasVerified || justVerify(event)) { - oldChannel.updateChannelInfo(author, event, note) - true - } else { - false - } - } else { - wasVerified - } - - if (note.event == null && (isVerified || justVerify(event))) { - oldChannel.addNote(note, relay) - note.loadEvent(event, author, emptyList()) - - refreshNewNoteObservers(note) - } - - return isVerified - } - - fun consume( - event: ChannelMessageEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val channelId = event.channelId() ?: return false - - val new = consumeRegularEvent(event, relay, wasVerified) - - 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) - } - - return new - } - - fun consume( - event: EphemeralChatEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val roomId = event.roomId() ?: return false - - val new = consumeRegularEvent(event, relay, wasVerified) - - if (new) { - val note = getOrCreateNote(event.id) - val channel = getOrCreateEphemeralChannel(roomId) - channel.addNote(note, relay) - } - - return new - } - - /** - * Public geohash chat message (kind 20000). Routes into the cell's - * [GeohashChatChannel]. Presence (kind 20001) is deliberately NOT consumed - * here — it is an empty-content heartbeat handled by the live chat screen, so - * it never becomes a room's "last message". - */ - fun consume( - event: GeohashChatEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val geohash = event.geohash() ?: return false - - val new = consumeRegularEvent(event, relay, wasVerified) - - if (new) { - val note = getOrCreateNote(event.id) - val channel = getOrCreateGeohashChannel(geohash) - channel.addNote(note, relay) - } - - return new - } - - /** - * NIP-29 addressables (kinds 39000-39005) are authoritative for a group's metadata, - * roster, roles and pins ONLY when signed by the relay's own key — the NIP-11 `self` - * pubkey. This returns false only when we can positively tell an event is NOT relay-signed - * (the relay advertises a `self` and the event's author differs), so a stray or malicious - * user-published 39000/39001/… served by a lax relay can't overwrite a group's state (e.g. - * inject itself into the admin list). When `self` isn't known yet — the NIP-11 doc hasn't - * loaded, or the relay doesn't advertise one — we don't block, so legitimate groups still - * populate and this never regresses a relay whose key we simply haven't fetched. - */ - private fun isRelaySignedGroupEvent( - event: Event, - relay: NormalizedRelayUrl, - ): Boolean { - val self = appHost.relaySelfPubKey(relay) ?: return true - return event.pubKey == self - } - - /** - * NIP-29 relay-signed group metadata (kind 39000). Stored as an addressable - * note and used to populate the [RelayGroupChannel]'s name/picture/about/ - * flags. The group is keyed by (host relay + group id): unlike NIP-C7, a - * NIP-29 event does not carry its host relay in a tag — the host is the relay - * that served it — so the channel can only be associated when we know the - * serving relay (provenance). With no relay we still store the metadata. - */ - fun consume( - event: GroupMetadataEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val new = consumeBaseReplaceable(event, relay, wasVerified) - - if (relay != null && isRelaySignedGroupEvent(event, relay)) { - val note = getOrCreateAddressableNote(event.address()) - val channel = getOrCreateRelayGroupChannel(GroupId(event.groupId(), relay)) - (note.event as? GroupMetadataEvent)?.let { channel.updateGroupInfo(it, note) } - } - - return new - } - - /** NIP-29 relay-signed member list (kind 39002) → the group's roster. */ - fun consume( - event: GroupMembersEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val new = consumeBaseReplaceable(event, relay, wasVerified) - if (relay != null && isRelaySignedGroupEvent(event, relay)) { - val latest = getOrCreateAddressableNote(event.address()).event as? GroupMembersEvent - latest?.let { getOrCreateRelayGroupChannel(GroupId(it.groupId(), relay)).updateMembers(it) } - } - return new - } - - /** NIP-29 relay-signed admin list (kind 39001) → the group's roster. */ - fun consume( - event: GroupAdminsEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val new = consumeBaseReplaceable(event, relay, wasVerified) - if (relay != null && isRelaySignedGroupEvent(event, relay)) { - val latest = getOrCreateAddressableNote(event.address()).event as? GroupAdminsEvent - latest?.let { getOrCreateRelayGroupChannel(GroupId(it.groupId(), relay)).updateAdmins(it) } - } - return new - } - - /** NIP-29 relay-signed pinned-message list (kind 39005) → the group's pins. */ - fun consume( - event: GroupPinnedEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val new = consumeBaseReplaceable(event, relay, wasVerified) - if (relay != null && isRelaySignedGroupEvent(event, relay)) { - val latest = getOrCreateAddressableNote(event.address()).event as? GroupPinnedEvent - latest?.let { getOrCreateRelayGroupChannel(GroupId(it.groupId(), relay)).updatePinned(it) } - } - return new - } - - /** NIP-29 relay-declared supported roles (kind 39003) → the group's role set. */ - fun consume( - event: SupportedRolesEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val new = consumeBaseReplaceable(event, relay, wasVerified) - if (relay != null && isRelaySignedGroupEvent(event, relay)) { - val latest = getOrCreateAddressableNote(event.address()).event as? SupportedRolesEvent - latest?.let { getOrCreateRelayGroupChannel(GroupId(it.groupId(), relay)).updateSupportedRoles(it) } - } - return new - } - - /** - * Buzz/NIP-43 community (relay-wide) membership snapshot (kind 13534) → the relay's community roster. - * Buzz grants participation across ALL its channels off this list, not the per-channel 39002, so - * feeding it into [BuzzCommunityMembership] lets [RelayGroupChannel.membershipOf] recognise a - * community member even when a channel roster omits them. Scoped to Buzz relays (the concept is - * Buzz-specific and the event is relay-signed + NIP-70 relay-only). - */ - fun consume( - event: RelayMembershipListEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val new = consumeBaseReplaceable(event, relay, wasVerified) - if (relay != null && BuzzRelayDialect.isBuzz(relay)) { - if (BuzzCommunityMembership.updateSnapshot(relay, event.members().toSet(), event.createdAt)) { - refreshRelayGroupMembership(relay) - } - } - return new - } - - /** Buzz/NIP-43 community member-added delta (kind 8000) → adds to the relay's [BuzzCommunityMembership]. */ - fun consume( - event: RelayAddMemberEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val new = consumeBuzzRegularEvent(event, relay, wasVerified) - if (relay != null && BuzzRelayDialect.isBuzz(relay)) { - if (BuzzCommunityMembership.applyDelta(relay, add = event.memberPubKeys().toSet(), createdAt = event.createdAt)) { - refreshRelayGroupMembership(relay) - } - } - return new - } - - /** Buzz/NIP-43 community member-removed delta (kind 8001) → removes from the relay's [BuzzCommunityMembership]. */ - fun consume( - event: RelayRemoveMemberEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val new = consumeBuzzRegularEvent(event, relay, wasVerified) - if (relay != null && BuzzRelayDialect.isBuzz(relay)) { - if (BuzzCommunityMembership.applyDelta(relay, remove = event.memberPubKeys().toSet(), createdAt = event.createdAt)) { - refreshRelayGroupMembership(relay) - } - } - return new - } - - /** - * Poke every relay-group channel on [relay] so the `membershipOf` community-level fallback re-renders - * (community membership lives outside the per-channel roster, so nothing else invalidates their state). - */ - private fun refreshRelayGroupMembership(relay: NormalizedRelayUrl) { - getRelayGroupChannelsOnRelay(relay).forEach { it.updateChannelInfo() } - } - - /** - * Marks the serving relay as Buzz, but only off a VERIFIED event: the mark changes - * what the composer sends (40002 vs kind 9) and how new channels on the relay are - * treated, so an unverifiable frame from a buggy/hostile relay must not flip it. - * The note-has-event check is the same verification gate the attach path uses. - */ - private fun markBuzzIfVerified( - event: Event, - relay: NormalizedRelayUrl?, - ) { - if (relay != null && getNoteIfExists(event.id)?.event != null) { - BuzzRelayDialect.mark(relay) - } - } - - /** - * Consume + channel-attach for Buzz workspace timeline kinds (stream messages, - * diffs, system rows, forum posts, job cards, huddle lifecycle). These kinds only - * exist on `block/buzz` relays, so their (verified) arrival IS the dialect - * detection. Attachment goes through the SAME shared NIP-29 path kind-9 chat uses - * — including its stray-redirect protection — so mixed vanilla/Buzz conversations - * share one timeline and non-host strays never mint phantom channels. - */ - private fun consumeBuzzTimelineEvent( - event: Event, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean = - consumeRegularEvent(event, relay, wasVerified).also { - markBuzzIfVerified(event, relay) - attachToRelayGroupIfScoped(event, relay) - } - - /** - * A Buzz kind-40099 system message. It renders as a narration row in the channel feed (via - * [consumeBuzzTimelineEvent]), but a `channel_deleted` one is also the **authoritative signal that - * a channel is gone**: the relay soft-deletes the channel and its 39000/39001/39002 discovery - * events but emits no member-removed notification and never retracts the kind-44100 that seeds the - * browse list — so without this the deleted channel keeps re-appearing (a stale 44100 re-announced - * every restart, its metadata now blank so it shows optimistically). Recording the delete in - * [RelayGroupDeletions] filters it out of every list and persists it across restarts. - * - * Gated on [isRelaySignedGroupEvent] (the 40099 is signed by the relay keypair) so a spoofed - * system message from a stray author can't hide a channel. Cross-device by construction: the relay - * replays this on subscribe, so a channel deleted on Buzz web/desktop is honored here too. - */ - private fun consume( - event: SystemMessageEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean = - consumeBuzzTimelineEvent(event, relay, wasVerified).also { - if (relay != null && isRelaySignedGroupEvent(event, relay) && event.payload()?.type == SystemMessagePayload.CHANNEL_DELETED) { - event.channel()?.let { channelId -> RelayGroupDeletions.markDeleted(GroupId(channelId, relay)) } - } - } - - /** Store-only consume for Buzz kinds that carry no channel timeline row. */ - private fun consumeBuzzRegularEvent( - event: Event, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean = - consumeRegularEvent(event, relay, wasVerified).also { - markBuzzIfVerified(event, relay) - } - - /** - * A Buzz forum ROOT post (kind 45001): a thread, not a chat message. Route it to the group's - * Threads collection ([RelayGroupChannel.addThread]) — the same place kind-11 threads go — so it - * surfaces in the forum/Threads view instead of leaking into the kind-9 chat feed as a bubble. - * Its kind-45003 comments are stored (store-only) and loaded on demand by the thread detail. - */ - private fun consumeBuzzForumPost( - event: Event, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean = - consumeRegularEvent(event, relay, wasVerified).also { - markBuzzIfVerified(event, relay) - attachThreadToRelayGroupIfScoped(event, relay) - } - - private fun consume( - event: StreamMessageEditEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean = - // Buzz's own timeline set excludes 40003: an edit is an OVERLAY replacing an - // earlier message's content, never a row of its own. Store it and anchor it to the - // message it edits (Note.edits) — like every other edit kind — so the overlay is held - // for as long as its message and never leaks into the timeline as a bubble. - consumeBuzzRegularEvent(event, relay, wasVerified).also { - val target = event.editedMessage() ?: return@also - val editNote = getOrCreateNote(event.id) - if (editNote.event != null) { - getOrCreateNote(target).addEdit(editNote) - } - } - - private fun consume( - event: DmVisibilityEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean = - // The relay-signed, `#p`-gated per-viewer hidden-DM snapshot (30622). Store the - // addressable event AND mirror the viewer's hidden set into the registry so a - // hidden DM drops out of that viewer's list until it's re-opened. - consumeBaseReplaceable(event, relay, wasVerified).also { - val viewer = event.viewer().ifBlank { return@also } - BuzzDmRegistry.recordHidden(viewer, event.hiddenChannels().toSet()) - } - - private fun consume( - event: CanvasEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean = - // The canvas is the channel's single living document, not a chat row: track - // only the newest revision as overlay state, never attach it to the timeline. - consumeBuzzRegularEvent(event, relay, wasVerified).also { - val channelId = event.channel() ?: return@also - val note = getOrCreateNote(event.id) - if (note.event != null) { - BuzzWorkspaceStates.getOrCreate(channelId).updateCanvas(note) - } - } - - /** - * A kind-44101 "you were removed from a channel". Stored like any other Buzz event and nothing more: - * withdrawing the matching add-prompt is not a side effect of ingest but a consequence of the stored - * event, since - * [com.vitorpamplona.amethyst.commons.model.buzz.BuzzChannelInvites.pendingInvites] resolves each - * channel to its newest verdict. That ordering is what makes the two kinds arriving out of order — - * routine on a re-subscribe, where the relay replays the whole history — produce the same answer as - * them arriving in order. - */ - private fun consume( - event: MemberRemovedNotificationEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean = consumeBuzzRegularEvent(event, relay, wasVerified) - - /** - * Attach a group-scoped content event (a kind-9 chat, kind-1068 poll, … - * carrying an `h` tag) to its [RelayGroupChannel]. NIP-29 reuses the generic - * content kinds and scopes them with `h`, so the note is consumed normally - * and then, when it belongs to a group and we know the serving relay, added - * to that group's channel timeline. - */ - private fun attachToRelayGroupIfScoped( - event: Event, - relay: NormalizedRelayUrl?, - ) { - val groupId = event.groupId() ?: return - val note = getOrCreateNote(event.id) - // Only attach a note we've actually loaded — never a placeholder for an - // unverified/not-yet-seen event. This is checked here (not via the "was - // newly consumed" flag) so the host relay's echo of an event we already - // stored from our own send still lands in the channel. - if (note.event == null) return - - if (relay != null) { - val exact = GroupId(groupId, relay) - val existing = getRelayGroupChannelIfExists(exact) - if (existing != null) { - // Normal arrival: the group's host-pinned filters served it, so the serving relay IS the - // group's key and its channel already exists. Fast O(1) path — no scan. - existing.addNote(note, relay) - } else { - // No channel keyed to the serving relay: this may be a stray from a NON-host relay (e.g. a - // quoted kind-9 resolved by id). Redirect it to the group's single confirmed host rather - // than mint a phantom channel the group's screens never read (the serving-relay hazard); - // fall back to the serving-relay key when there is no single host (new/ambiguous group). - val target = redirectStrayRelayGroupContent(relayGroupCandidatesFor(groupId)) ?: exact - getOrCreateRelayGroupChannel(target).addNote(note, relay) - } - } else { - // Our own optimistic send has no provenance relay, so we can't build the (groupId, - // relay) key. Attach only when a SINGLE open channel has this group id (the room being - // composed in). When the id is ambiguous across relays — e.g. the relay-wide "_" group - // joined on several relays — skip: attaching to all of them bleeds the message into - // rooms it wasn't sent to. The host relay's echo (relay != null) lands it on the right key. - relayGroupChannels - .filter { key, _ -> key.id == groupId } - .singleOrNull() - ?.addNote(note, null) - } - } - - /** Candidate group channels for the [redirectStrayRelayGroupContent] slow path — one scan by group id. */ - private fun relayGroupCandidatesFor(groupId: String): List = - relayGroupChannels - .filter { key, _ -> key.id == groupId } - .map { RelayGroupTargetCandidate(it.groupId, it.hasRelaySignedState()) } - - /** - * Same routing as [attachToRelayGroupIfScoped] but for kind-11 threads, which - * are kept in a separate collection from the chat timeline so the two content - * types don't mix in one feed. - */ - private fun attachThreadToRelayGroupIfScoped( - event: Event, - relay: NormalizedRelayUrl?, - ) { - val groupId = event.groupId() ?: return - val note = getOrCreateNote(event.id) - if (note.event == null) return - - if (relay != null) { - val exact = GroupId(groupId, relay) - val existing = getRelayGroupChannelIfExists(exact) - if (existing != null) { - existing.addThread(note) - } else { - // Same serving-relay hazard as the chat path: prefer the single confirmed host over a phantom. - val target = redirectStrayRelayGroupContent(relayGroupCandidatesFor(groupId)) ?: exact - getOrCreateRelayGroupChannel(target).addThread(note) - } - } else { - // See attachToRelayGroupIfScoped: only attach when the group id is unambiguous. - relayGroupChannels - .filter { key, _ -> key.id == groupId } - .singleOrNull() - ?.addThread(note) - } - } - - fun consume( - event: LiveActivitiesChatMessageEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val activityAddress = event.activityAddress() ?: return false - - val new = consumeRegularEvent(event, relay, wasVerified) - - if (new) { - val channel = getOrCreateLiveChannel(activityAddress) - val note = getOrCreateNote(event.id) - channel.addNote(note, relay) - } - - return new - } - - /** - * Audio-room presence (kind-10312) — addressable storage plus an - * author-keyed entry in the room's - * [LiveActivitiesChannel.presenceNotes] index. - * - * Presence is intentionally NOT added to `channel.notes`: that - * map is dominated by chat in active rooms and feeds that care - * about presence (Nests drawer, home live-bubble, NestsFeedLoaded) - * iterate `presenceNotes` directly for an O(speakers) scan. - * - * Cross-room move handling: kind-10312 is replaceable per author, - * but the room a presence points to (`a`-tag) can change when a - * speaker hops between rooms. The replaceable cache only swaps - * the addressable's content — it has no notion of which channel - * the old version was attached to. Without explicit eviction the - * old room would keep surfacing as "live" via the stale entry - * until it dropped out of the freshness window. Capture the prior - * room before replacement and, when it differs, drop the author - * from the old channel's presence index. - */ - fun consume( - event: MeetingRoomPresenceEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val priorVersion = getAddressableNoteIfExists(event.address())?.event as? MeetingRoomPresenceEvent - val priorRoomAddress = priorVersion?.interactiveRoom()?.address - val isReplacement = priorVersion != null && event.createdAt > priorVersion.createdAt - - val new = consumeBaseReplaceable(event, relay, wasVerified) - - val roomAddress = event.interactiveRoom()?.address ?: return new - if (roomAddress.kind != MeetingSpaceEvent.KIND) return new - - if (isReplacement && priorRoomAddress != null && priorRoomAddress != roomAddress) { - getLiveActivityChannelIfExists(priorRoomAddress)?.removePresenceNote(event.pubKey) - } - - val channel = getOrCreateLiveChannel(roomAddress) - val versionNote = getOrCreateNote(event.id) - channel.addPresenceNote(versionNote) - if (relay != null) channel.addRelay(relay) - - return new - } - - fun consume( - event: LiveActivitiesRaidEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val fromAddress = event.fromAddress() - val toAddress = event.toAddress() - if (fromAddress == null && toAddress == null) return false - - val new = consumeRegularEvent(event, relay, wasVerified) - - if (new) { - val note = getOrCreateNote(event.id) - fromAddress?.let { getOrCreateLiveChannel(it).addNote(note, relay) } - toAddress?.let { getOrCreateLiveChannel(it).addNote(note, relay) } - } - - return new - } - - fun consume( - event: LiveActivitiesClipEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val activityAddress = event.activityAddress() ?: return false - - val new = consumeRegularEvent(event, relay, wasVerified) - - if (new) { - val channel = getOrCreateLiveChannel(activityAddress) - val note = getOrCreateNote(event.id) - channel.addNote(note, relay) - } - - return new - } - - @Suppress("UNUSED_PARAMETER") - fun consume( - event: ChannelHideMessageEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean = false - - @Suppress("UNUSED_PARAMETER") - fun consume( - event: ChannelMuteUserEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean = false - - fun consume( - event: LnZapEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val note = getOrCreateNote(event.id) - - // Already processed this event — still ensure it's routed into any live-activity - // channel(s) it references. A zap that was first consumed by, e.g., the notifications - // subscription must still appear in the stream's chat when the user opens the stream. - if (note.event != null) { - attachZapToLiveActivityChannel(event, note, relay) - return false - } - - if (!(wasVerified || justVerify(event))) return false - - val existingZapRequest = event.zapRequest?.id?.let { getNoteIfExists(it) } - if (existingZapRequest == null || existingZapRequest.event == null) { - // tries to add it - event.zapRequest?.let { - checkDeletionAndConsume(it, relay, false) - } - } - - val zapRequest = event.zapRequest?.id?.let { getNoteIfExists(it) } - - if (zapRequest == null || zapRequest.event !is LnZapRequestEvent) { - Log.d("ZP") { "Zap Request not found. Unable to process Zap {${event.toJson()}}" } - return false - } - - // NIP-57 Appendix F validation. Resolve the recipient's lnurl from their - // profile metadata, look up the LNURL provider's `nostrPubkey`, and check - // the receipt against it (signer + invoice amount + lnurl tag). - // - // Synchronous path: cache hit, or no resolver wired (fallback to legacy - // signature-only behavior). Failed MUST-checks drop the receipt entirely. - // - // Async path: cache miss + resolver available. Load the event so feeds - // and live channels see it, but defer the `addZap` credit until the - // resolver returns. On async MUST-fail we never credit; the receipt - // stays in cache as a visible artifact but contributes 0 to zap totals. - val recipientLnurl = recipientLnurl(event) - val recipientLnurlpUrl = recipientLnurl?.let { LnurlForm.toUrl(it) } - val cachedInfo = recipientLnurlpUrl?.let { LnurlEndpointCache.get(it) } - - val author = getOrCreateUser(event.pubKey) - val repliesTo = computeReplyTo(event) - - if (cachedInfo != null) { - val result = - LnZapReceiptValidator.validate( - receipt = event, - expectedNostrPubkey = cachedInfo.nostrPubkey, - expectedLnurl = recipientLnurl, - ) - if (result is LnZapReceiptValidator.Result.Invalid && - result.reason != LnZapReceiptValidator.Result.Reason.MISMATCHED_LNURL - ) { - Log.w("ZP") { "dropping zap receipt ${event.id}: ${result.reason} ${result.detail ?: ""}" } - return false - } - if (result is LnZapReceiptValidator.Result.Invalid) { - Log.w("ZP") { "zap receipt ${event.id} has mismatched lnurl tag (accepting per SHOULD)" } - } - - note.loadEvent(event, author, repliesTo) - repliesTo.forEach { it.addZap(zapRequest, note) } - attachZapToLiveActivityChannel(event, note, relay) - refreshNewNoteObservers(note) - return true - } - - val resolver = lnurlEndpointResolver - if (resolver == null || recipientLnurlpUrl == null) { - // No resolver configured, or recipient has no lud16/lud06 on file. - // Legacy behavior: accept the receipt on signature verification alone. - note.loadEvent(event, author, repliesTo) - repliesTo.forEach { it.addZap(zapRequest, note) } - attachZapToLiveActivityChannel(event, note, relay) - refreshNewNoteObservers(note) - return true - } - - // Async validation. Make the event visible immediately, but only credit - // it to repliesTo after the resolver confirms the LNURL provider's pubkey. - note.loadEvent(event, author, repliesTo) - attachZapToLiveActivityChannel(event, note, relay) - refreshNewNoteObservers(note) - - appHost.scope.launch { - try { - val info = resolver.resolve(recipientLnurlpUrl) - if (info == null) { - Log.w("ZP") { "could not fetch lnurlp for ${event.id}; not crediting" } - return@launch - } - val result = - LnZapReceiptValidator.validate( - receipt = event, - expectedNostrPubkey = info.nostrPubkey, - expectedLnurl = recipientLnurl, - ) - if (result is LnZapReceiptValidator.Result.Invalid && - result.reason != LnZapReceiptValidator.Result.Reason.MISMATCHED_LNURL - ) { - Log.w("ZP") { "dropping zap receipt ${event.id}: ${result.reason} ${result.detail ?: ""}" } - return@launch - } - if (result is LnZapReceiptValidator.Result.Invalid) { - Log.w("ZP") { "zap receipt ${event.id} has mismatched lnurl tag (accepting per SHOULD)" } - } - repliesTo.forEach { it.addZap(zapRequest, note) } - } catch (t: Throwable) { - Log.w("ZP", "validation failed for ${event.id}", t) - } - } - - return true - } - - /** - * Look up the recipient's lnurl from their kind:0 metadata. The recipient is - * the receipt's first `p` tag (which the LNURL provider sets to the original - * zap target). Returns null if we have no metadata, or the user has neither - * lud16 nor lud06. - */ - private fun recipientLnurl(event: LnZapEvent): String? { - val recipientPubkey = event.zappedAuthor().firstOrNull() ?: return null - val user = getUserIfExists(recipientPubkey) ?: return null - return user.lnAddress()?.takeIf { it.isNotBlank() } - } - - fun consume( - event: OnchainZapEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val note = getOrCreateNote(event.id) - val alreadyLoaded = note.event != null - - // `relay == null` means this event was generated locally by the signed-in user - // and routed through `justConsumeMyOwnEvent` — see `Account.sendOnchainZap` → - // `LocalCache.justConsumeMyOwnEvent`. Only these get the optimistic gallery - // attachment; for incoming zaps from others, the sender-claimed `amount` tag - // is untrusted and could mislead the viewer until the chain verifier responds. - val isOwnEvent = relay == null - var repliesTo: List? = null - - if (!alreadyLoaded) { - if (!(wasVerified || justVerify(event))) return false - - // Anti-spoofing: NIP-BC requires rejecting self-zaps. - val recipient = event.recipient() ?: return false - if (event.pubKey.equals(recipient, ignoreCase = true)) return false - - val author = getOrCreateUser(event.pubKey) - val resolvedRepliesTo = computeReplyTo(event) - repliesTo = resolvedRepliesTo - note.loadEvent(event, author, resolvedRepliesTo) - - if (isOwnEvent) { - // Optimistic attachment for the sender's own zap: surface it on the - // thread immediately, before the chain backend has indexed the tx. - // Crucial because `OnchainZapSender.send` consumes the kind:8333 - // milliseconds after broadcasting the tx — the verifier would - // otherwise return TX_NOT_FOUND and the entry would never appear - // until a later re-verification pass. - val txid = event.txid() - if (txid != null) { - // Clamp claimedSats to a non-negative value. `amount` tag parses - // via toLongOrNull() with no sign check, so a malicious sender - // could otherwise put "-1" in the gallery as a negative-sats badge. - val claimedSats = (event.claimedAmountInSats() ?: 0L).coerceAtLeast(0L) - resolvedRepliesTo.forEach { - it.addOnchainZap(note, txid, claimedSats, verifiedSats = 0L, OnchainZapStatus.UNVERIFIED) - } - } - } - - refreshNewNoteObservers(note) - } - - // Async chain verification is delegated to OnchainZapResolver, which owns the - // in-flight gates (so two relay echoes don't double-fetch) and the chain-tip - // polling flow used by the gallery driver. Reusing the repliesTo already - // computed above avoids the second computeReplyTo pass on the new-event path. - onchainZapResolver.launchVerification(event, note, repliesTo ?: computeReplyTo(event)) - - return !alreadyLoaded - } - - fun consume( - event: Bolt12ZapEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val note = getOrCreateNote(event.id) - - // Already processed — still route it into any live-activity channel it references. - if (note.event != null) { - attachZapToLiveActivityChannel(event, note, relay) - return false - } - - if (!(wasVerified || justVerify(event))) return false - - // NIP-B1 validation is fully synchronous: zap-event structure, the embedded - // kind:9737 intent match, and the `lnp` payer-proof binding + crypto. A failed - // validation drops the zap entirely — it never contributes to a zap total. - // The outer event signature was already verified above (wasVerified/justVerify), - // so skip the redundant re-check inside the validator. - val validation = bolt12ZapValidator.validate(event, verifyEventSignature = false) - if (validation !is Bolt12ZapValidation.Valid) { - Log.w("ZP") { "dropping bolt12 zap ${event.id}: ${(validation as Bolt12ZapValidation.Invalid).reason}" } - return false - } - - val author = getOrCreateUser(event.pubKey) - val repliesTo = computeReplyTo(event) - note.loadEvent(event, author, repliesTo) - repliesTo.forEach { - it.addBolt12Zap(note, validation.paymentHashHex, validation.amountMillisats, validation.proofCryptoVerified) - } - attachZapToLiveActivityChannel(event, note, relay) - refreshNewNoteObservers(note) - return true - } - - /** - * Consume a NIP-61 nutzap (kind 9321). Resolves the e-tagged target - * note(s), parses the proof amounts once, and attaches a `NutzapEntry` - * so the reaction-row counter, the "you-already-zapped" icon highlight, - * and the dedicated cashu gallery row all light up the same way they - * do for lightning zaps. - * - * Unlike `consume(OnchainZapEvent)`, there's no async verification - * step today — the sender-claimed proof amounts are trusted up to - * redeem time, when the recipient's wallet verifies them against the - * mint. Adding a wallet-side verification gate that downgrades - * unverifiable entries would be a follow-up. - */ - fun consume( - event: NutzapEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val note = getOrCreateNote(event.id) - if (note.event != null) return false - - if (!(wasVerified || justVerify(event))) return false - - val author = getOrCreateUser(event.pubKey) - val repliesTo = computeReplyTo(event) - note.loadEvent(event, author, repliesTo) - - val claimedSats = event.claimedSatsTotal() - repliesTo.forEach { it.addNutzap(note, claimedSats) } - - refreshNewNoteObservers(note) - return true - } - - private fun attachZapToLiveActivityChannel( - event: LnZapEvent, - note: Note, - relay: NormalizedRelayUrl?, - ) { - // Match zap.stream: only show zaps whose receiver is the live activity host. - val hosts = event.zappedAuthor().toHashSet() - if (hosts.isEmpty()) return - - // Route into every live-activity address this zap references (zap.stream uses one, but - // a receipt could legitimately reference multiple simulcasted streams). - event.tags - .asSequence() - .mapNotNull(ATag::parseAddress) - .filter { it.kind == LiveActivitiesEvent.KIND && it.pubKeyHex in hosts } - .distinct() - .forEach { address -> - getOrCreateLiveChannel(address).addNote(note, relay) - } - } - - private fun attachZapToLiveActivityChannel( - event: Bolt12ZapEvent, - note: Note, - relay: NormalizedRelayUrl?, - ) { - // Only surface zaps whose recipient is the live activity host. - val host = event.recipient() ?: return - event.tags - .asSequence() - .mapNotNull(ATag::parseAddress) - .filter { it.kind == LiveActivitiesEvent.KIND && it.pubKeyHex == host } - .distinct() - .forEach { address -> - getOrCreateLiveChannel(address).addNote(note, relay) - } - } - - fun consume( - event: LnZapRequestEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val note = getOrCreateNote(event.id) - - // Already processed this event. - if (note.event != null) return false - - if (wasVerified || justVerify(event)) { - val author = getOrCreateUser(event.pubKey) - val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } - val repliesTo = computeReplyTo(event) - - note.loadEvent(event, author, repliesTo) - - repliesTo.forEach { it.addZap(note, null) } - - refreshNewNoteObservers(note) - - return true - } - - return false - } - - fun consume( - event: TextNoteModificationEvent, - 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()) - - // Anchor the modification to the note it edits, like every other edit kind — the read side - // (Note.textNoteModifications) then folds `edited.edits` instead of scanning the cache, - // and addEdit invalidates the note's edits flow so the UI re-derives. - event.editedNote()?.let { - checkGetOrCreateNote(it.eventId)?.addEdit(note) - } - - refreshNewNoteObservers(note) - - return true - } - - return false - } - - fun consume( - event: ConcordChatEditEvent, - 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 - - // A Concord edit rumor is unsigned (its sig is empty); the envelope open path already - // established authenticity, so we consume it as pre-verified like any other Concord rumor. - if (wasVerified || justVerify(event)) { - note.loadEvent(event, author, emptyList()) - - // Anchor the edit to the message it edits (like a reaction to its target), so it survives - // as long as that channel-retained message does. A Concord rumor is decrypted exactly once - // per session — the community session dedups re-delivered wraps — so an edit left orphaned - // in the soft cache could be GC'd and never re-downloaded. The bubble reads `note.edits`. - event.editedMessageId()?.let { targetId -> - getOrCreateNote(targetId).addEdit(note) - } - - refreshNewNoteObservers(note) - - return true - } - - return false - } - - fun consume( - event: PollResponseEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val pollId = event.poll()?.eventId - if (pollId != null) { - val pollNote = getOrCreateNote(pollId) - val responseNote = getOrCreateNote(event.id) - - val new = consumeRegularEvent(event, relay, wasVerified) - if (new) { - pollNote.pollState().addResponse(responseNote) - // Responses and their poll race each other. If the poll is already here, hand the - // tally its rules now; if it isn't, consume(PollEvent) does it on arrival. - (pollNote.event as? PollEvent)?.let { - pollNote.pollState().updatePolicy(PollTallyPolicy.from(it)) - } - } - return new - } - - return false - } - - fun consume( - event: PollEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val new = consumeRegularEvent(event, relay, wasVerified) - attachToRelayGroupIfScoped(event, relay) - - // Not gated on `new`: the tally may have been built from responses that arrived before this - // poll did, and updatePolicy is idempotent for the usual re-delivery from another relay. - getOrCreateNote(event.id).pollState().updatePolicy(PollTallyPolicy.from(event)) - - return new - } - - fun consume( - event: FileStorageEvent, - 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) - } - - // A host with no blob directory keeps the bytes in memory below instead of spilling them. - val cachePath = appHost.nip95BlobDir - - val isVerified = - if (cachePath == null) { - wasVerified - } else { - try { - cachePath.mkdirs() - val file = File(cachePath, event.id) - if (!file.exists() && (wasVerified || justVerify(event))) { - FileOutputStream(file).use { stream -> - stream.write(event.decode()) - } - Log.i( - "FileStorageEvent", - "NIP95 File received from $relay and saved to disk as $file", - ) - true - } else { - wasVerified - } - } catch (e: IOException) { - Log.e("FileStorageEvent", "FileStorageEvent save to disk error: " + event.id, e) - wasVerified - } - } - - // Already processed this event. - if (note.event != null) return false - - if (isVerified || justVerify(event)) { - // The blob is on disk now, so the copy in the note drops its content. Without a - // directory to spill to there is nowhere else for it to live, so keep it whole. - val stored = - if (cachePath != null) { - FileStorageEvent(event.id, event.pubKey, event.createdAt, event.tags, "", event.sig) - } else { - event - } - - note.loadEvent(stored, author, emptyList()) - - refreshNewNoteObservers(note) - - return true - } - - return false - } - - fun consume( - event: LnZapPaymentRequestEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - // Does nothing without a response callback. - return true - } - - fun consume( - event: LnZapPaymentRequestEvent, - zappedNote: Note?, - wasVerified: Boolean, - relay: NormalizedRelayUrl?, - onResponse: suspend (LnZapPaymentResponseEvent) -> Unit, - ): Boolean { - val note = getOrCreateNote(event.id) - val author = getOrCreateUser(event.pubKey) - - // Already processed this event. - if (note.event != null) return false - - if (wasVerified || justVerify(event)) { - val expectedServicePubkey = - event.walletServicePubKey() ?: run { - Log.w("LocalCache") { "NWC request ${event.id} has no `p` tag; cannot register for response." } - return false - } - - note.loadEvent(event, author, emptyList()) - - relay?.let { - note.addRelay(relay) - } - - zappedNote?.addZapPayment(note, null) - - paymentTracker.registerRequest(event.id, expectedServicePubkey, zappedNote, onResponse) - - refreshNewNoteObservers(note) - - return true - } - - return false - } - - fun consume( - event: LnZapPaymentResponseEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - val requestId = event.requestId() - - // Duplicate delivery, checked before the tracker so the warnings below mean one - // thing each. Some NWC relays replay every cached kind-23195 whenever the REQ - // filter changes (see NWCPaymentFilterAssembler), so an already-answered response - // arrives again and again. Its first copy consumed the pending request, so the - // replays would otherwise be reported as "no pending request is registered" — - // the same line a genuinely late response produces, which made the two - // indistinguishable in the field. - if (getNoteIfExists(event.id)?.event != null) return false - - val pending = - when (val match = paymentTracker.onResponseReceived(requestId, event.pubKey)) { - is NwcPaymentTracker.MatchResult.Matched -> { - match.pending - } - - is NwcPaymentTracker.MatchResult.WrongAuthor -> { - // Possible spoof: a kind-23195 event from someone other than - // the wallet service we sent the request to. The pending - // entry is left in place so the real response can still - // resolve it; we silently drop this one. - Log.w("LocalCache") { - "Rejecting NWC response ${event.id}: expected author ${match.expected} but event was signed by ${match.actual}. " + - "This may be a spoofed reply — keeping the request pending for the legitimate wallet response." - } - return false - } - - NwcPaymentTracker.MatchResult.NoMatch -> { - // Not a replay — those are filtered above — so this is the first time we - // have seen this response and nothing is waiting for it. - Log.w("LocalCache") { - "NWC response ${event.id} from ${event.pubKey} references request e=$requestId but no pending request is registered. " + - "The response arrived after the client gave up waiting, the user holds a stale subscription, " + - "or the wallet service set the wrong e tag." - } - return false - } - } - - val zappedNote = pending.zappedNote - val responseCallback = pending.onResponse - - val requestNote = requestId?.let { checkGetOrCreateNote(requestId) } - - val note = getOrCreateNote(event.id) - val author = getOrCreateUser(event.pubKey) - - // Backstop for a concurrent delivery that loaded the event between the replay - // check above and here. Same outcome, no warning: it is not a protocol problem. - if (note.event != null) return false - - if (wasVerified || justVerify(event)) { - note.loadEvent(event, author, emptyList()) - - requestNote?.let { request -> zappedNote?.addZapPayment(request, note) } - - appHost.scope.launch { - responseCallback(event) - } - - return true - } - - return false - } - - fun getPeopleListNotesFor(user: User): List = addressables.filter(PeopleListEvent.KIND, user.pubkeyHex) - - override fun markAsSeen( - eventId: String, - relay: NormalizedRelayUrl, - ) { - val note = getNoteIfExists(eventId) - - note?.event?.let { noteEvent -> - if (noteEvent is AddressableEvent) { - getAddressableNoteIfExists(noteEvent.address())?.addRelay(relay) - } - } - - note?.let { addRelayToNoteAndInners(it, relay) } - } - - /** - * Adds [relay] to [note] and to every already-unwrapped inner note of its - * gift-wrap chain (wrap → seal → rumor). The chat UI renders the inner - * rumor, so a relay recorded only on the outer envelope never surfaces as - * an icon. Inner notes that don't exist yet are not lost: the unwrap path - * copies the envelope's relays down via [copyRelaysFromTo] when it runs. - */ - fun addRelayToNoteAndInners( - note: Note, - relay: NormalizedRelayUrl, - ) { - note.addRelay(relay) - - val noteEvent = note.event - if (noteEvent is HasInnerEvent) { - noteEvent.innerEventId?.let { innerId -> - getNoteIfExists(innerId)?.let { addRelayToNoteAndInners(it, relay) } - } - } - } - - // Observers line up here. - val live: LocalCacheFlow = LocalCacheFlow() - - private fun refreshNewNoteObservers(newNote: Note) { - val event = newNote.event as Event - - // Index-driven fanout: only observers whose filter narrows - // on a field this event carries (or that registered as - // unindexed) get woken up. The match check inside each - // observer's `new()` still enforces negative constraints. - for (observer in observables.candidatesFor(event)) { - observer.new(event, newNote) - } - live.newNote(newNote) - } - - internal fun refreshDeletedNoteObservers(newNote: Note) { - // Deletes don't have a filterable shape — every observer - // might hold this note in its result set, so iterate them - // all. The index doesn't help here. - observables.forEach { it.remove(newNote) } - live.removedNote(newNote) - } - - /** - * Resource-usage ledger hook: called with (elapsedNanos, valid) for every - * signature verification so the app can account crypto CPU per day. - * Wired by AppModules like [onchainBackend]; null costs nothing. - */ - @Volatile - var verifyMeter: ((elapsedNanos: Long, valid: Boolean) -> Unit)? = null - - fun justVerify(event: Event): Boolean { - appHost.assertNotMainThread() - - val meter = verifyMeter - if (meter == null) return justVerifyInner(event) - - val start = System.nanoTime() - val valid = justVerifyInner(event) - meter(System.nanoTime() - start, valid) - return valid - } - - private fun justVerifyInner(event: Event): Boolean = - if (!event.verify()) { - try { - 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}" } - } - false - } else { - true - } - - fun consume( - event: DraftWrapEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean = !event.isDeleted() && consumeBaseReplaceable(event, relay, wasVerified) - - override fun consumeEmbedded(event: Event) { - justConsume(event, null, false) - } - - override fun justConsumeMyOwnEvent(event: Event) = justConsumeAndUpdateIndexes(event, null, true) - - fun justConsume( - event: Event, - relay: IRelayClient?, - wasVerified: Boolean, - ): Boolean { - if (deletionIndex.hasBeenDeleted(event)) { - // update relay with deletion event from another. - if (relay != null) { - deletionIndex.hasBeenDeletedBy(event)?.let { deletionEvent -> - getNoteIfExists(deletionEvent.id)?.let { note -> - if (!note.hasRelay(relay.url)) { - if (appHost.isDebug) { - Log.d("LocalCache") { "Updating ${relay.url.url} with a Deletion Event ${event.id} ${deletionEvent.id} because of ${event.toJson()} with ${deletionEvent.toJson()}" } - } - relay.sendIfConnected(EventCmd(deletionEvent)) - note.addRelay(relay.url) - } - } - } - } - return false - } - - if (event is AddressableEvent && relay != null) { - // updates relay with a new event. - getAddressableNoteIfExists(event.address())?.let { note -> - note.event?.let { existingEvent -> - if (existingEvent.createdAt > event.createdAt && !note.hasRelay(relay.url) && !deletionIndex.hasBeenDeleted(event) && !event.isExpired()) { - if (appHost.isDebug) { - Log.d("LocalCache") { "Updating ${relay.url.url} with a new version of ${event.kind} ${event.id} to ${existingEvent.id}" } - } - - relay.sendIfConnected(EventCmd(existingEvent)) - // only send once. - note.addRelay(relay.url) - } - } - } - } - - return justConsumeAndUpdateIndexes(event, relay?.url, wasVerified) - } - - fun checkDeletionAndConsume( - event: Event, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean = - if (!deletionIndex.hasBeenDeleted(event)) { - justConsumeAndUpdateIndexes(event, relay, wasVerified) - } else { - false - } - - private fun justConsumeAndUpdateIndexes( - event: Event, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean { - // uses the internal event to avoid reprocessing cached items. - val note = - if (event is AddressableEvent) { - getOrCreateAddressableNote(event.address()) - } else { - getOrCreateNote(event.id) - } - - val wasNew = justConsumeInnerInner(event, relay, wasVerified) - - if (wasNew) { - updateHintIndexes(event) - updateMintIndex(event) - updateInGatherer(event, note) - } - - if (relay != null) { - note.event?.let { consumedEvent -> - addIncomingRelayAsHintToAllRelatedEvents(consumedEvent, 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 { - 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) - } - } - } - - /** - * Feeds the Cashu mint URL directory from every event that names a - * mint, regardless of which user authored it. Three sources today: - * - * - NutzapInfoEvent (kind:10019) — every nostr user with a Cashu - * wallet publishes their accepted mints here, so a typical inbox - * of cached profiles seeds a useful starter directory. - * - MintRecommendationEvent (kind:38000) — explicit public vouches. - * - CashuMintEvent (kind:38172) — formal mint announcements. - * - * Called only when the event is newly consumed (mirrors - * updateHintIndexes) so a re-emission of a cached event doesn't - * inflate the popularity counter. - */ - fun updateMintIndex(event: Event) { - when (event) { - is NutzapInfoEvent -> mintDirectory.addAll(event.mints().map { it.mintUrl }) - is MintRecommendationEvent -> mintDirectory.addAll(event.mintUrls()) - is CashuMintEvent -> event.mintUrl()?.let { mintDirectory.add(it) } - else -> Unit - } - } - - fun updateInGatherer( - event: Event, - note: Note, - ) { - if (event is EventHintProvider) { - event.linkedEventIds().forEach { - checkGetOrCreateNote(it)?.addGatherer(note) - } - } - if (event is AddressHintProvider) { - event.linkedAddressIds().forEach { - checkGetOrCreateAddressableNote(it)?.addGatherer(note) - } - } - } - - @Suppress("DEPRECATION") - private fun justConsumeInnerInner( - event: Event, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ): Boolean = - try { - when (event) { - // ============================================================ - // Kinds with dedicated consume() logic: typed overloads that - // update channels, zaps, drafts, the deletion index, etc. - // Everything without per-kind logic falls through to the - // generic replaceable / regular groups at the bottom. - // ============================================================ - - is NutzapEvent -> consume(event, relay, wasVerified) - is ChannelCreateEvent -> consume(event, relay, wasVerified) - is ChannelHideMessageEvent -> consume(event, relay, wasVerified) - is ChannelMessageEvent -> consume(event, relay, wasVerified) - is ChannelMetadataEvent -> consume(event, relay, wasVerified) - is ChannelMuteUserEvent -> consume(event, relay, wasVerified) - is CommunityPostApprovalEvent -> consume(event, relay, wasVerified) - is DeletionEvent -> consume(event, relay, wasVerified) - is DraftWrapEvent -> consume(event, relay, wasVerified) - is EphemeralChatEvent -> consume(event, relay, wasVerified) - is GeohashChatEvent -> consume(event, relay, wasVerified) - is GroupMetadataEvent -> consume(event, relay, wasVerified) - is GroupMembersEvent -> consume(event, relay, wasVerified) - is GroupAdminsEvent -> consume(event, relay, wasVerified) - is GroupPinnedEvent -> consume(event, relay, wasVerified) - - // 39003 (relay-declared roles) is durable group state like 39000/39001/39002: - // route it onto the channel so a moderation UI can offer the relay's role set. - is SupportedRolesEvent -> consume(event, relay, wasVerified) - - is GenericRepostEvent -> consume(event, relay, wasVerified) - is FileStorageEvent -> consume(event, relay, wasVerified) - - is GiftWrapEvent -> { - // A wrap with an empty content carries no NIP-44 ciphertext and can - // never be unwrapped — reject it before paying for a signature check - // and a cache slot. Locally stripped copies (copyNoContent) are - // assigned straight to note.event and never pass through here. - if (event.content.isEmpty()) { - false - } else { - consumeRegularEvent(event, relay, wasVerified) - } - } - - is LiveActivitiesEvent -> consume(event, relay, wasVerified) - is LiveActivitiesChatMessageEvent -> consume(event, relay, wasVerified) - is LiveActivitiesRaidEvent -> consume(event, relay, wasVerified) - is LiveActivitiesClipEvent -> consume(event, relay, wasVerified) - is MeetingRoomPresenceEvent -> consume(event, relay, wasVerified) - - is PodcastMetadataEvent -> { - // Drop the known "Mock Podcast" spam flood instead of caching thousands of them. - if (event.isMockSpam()) { - false - } else { - consumeBaseReplaceable(event, relay, wasVerified) - } - } - - is LnZapEvent -> consume(event, relay, wasVerified) - is LnZapRequestEvent -> consume(event, relay, wasVerified) - is OnchainZapEvent -> consume(event, relay, wasVerified) - is Bolt12ZapEvent -> consume(event, relay, wasVerified) - is LnZapPaymentRequestEvent -> consume(event, relay, wasVerified) - is LnZapPaymentResponseEvent -> consume(event, relay, wasVerified) - is LongTextNoteEvent -> consume(event, relay, wasVerified) - is MetadataEvent -> consume(event, relay, wasVerified) - is NipTextEvent -> consume(event, relay, wasVerified) - is OtsEvent -> consume(event, relay, wasVerified) - is PollResponseEvent -> consume(event, relay, wasVerified) - is ReactionEvent -> consume(event, relay, wasVerified) - is LabelEvent -> consume(event, relay, wasVerified) - is ContactCardEvent -> consume(event, relay, wasVerified) - is ReportEvent -> consume(event, relay, wasVerified) - is RepostEvent -> consume(event, relay, wasVerified) - is StatusEvent -> consume(event, relay, wasVerified) - is TextNoteModificationEvent -> consume(event, relay, wasVerified) - is ConcordChatEditEvent -> consume(event, relay, wasVerified) - is WikiNoteEvent -> consume(event, relay, wasVerified) - is PaymentTargetsEvent -> consume(event, relay, wasVerified) - - is ChatEvent -> { - consumeRegularEvent(event, relay, wasVerified).also { - // Attach on every arrival, not just the newly-consumed one: - // our own send is consumed first with a null relay, so the - // host relay's later echo (new == false) is what carries the - // provenance needed to key the channel. attach is idempotent. - attachToRelayGroupIfScoped(event, relay) - } - } - - is PollEvent -> consume(event, relay, wasVerified) - - is ThreadEvent -> { - consumeRegularEvent(event, relay, wasVerified).also { - attachThreadToRelayGroupIfScoped(event, relay) - } - } - - // ------------------------------------------------------------------ - // Buzz workspace kinds (block/buzz — the Buzz dialect of NIP-29). - // Timeline kinds attach into the group's BuzzWorkspaceChannel; the - // rest are stored for query/state. Kinds 9041/39005/49001 are absent - // on purpose: their numbers belong to GoalEvent, GroupPinnedEvent and - // a non-wire audit kind. Kind 20001 is shared with GeohashPresenceEvent - // (BitChat); EventFactory routes it to PresenceUpdateEvent only when the - // BitChat `g` tag is absent, and it is handled below with the ephemerals. - // ------------------------------------------------------------------ - - is StreamMessageEditEvent -> consume(event, relay, wasVerified) - is SystemMessageEvent -> consume(event, relay, wasVerified) - is CanvasEvent -> consume(event, relay, wasVerified) - is MemberRemovedNotificationEvent -> consume(event, relay, wasVerified) - is RelayMembershipListEvent -> consume(event, relay, wasVerified) - is RelayAddMemberEvent -> consume(event, relay, wasVerified) - is RelayRemoveMemberEvent -> consume(event, relay, wasVerified) - is DmVisibilityEvent -> consume(event, relay, wasVerified) - - // Forum root (45001) is a thread, not a chat row → Threads collection. Comments (45003) - // and votes (45002) are store-only: the forum-thread detail loads them on demand by root. - is ForumPostEvent -> consumeBuzzForumPost(event, relay, wasVerified) - - is StreamMessageV2Event, - is StreamMessageDiffEvent, - is JobRequestEvent, - is JobAcceptedEvent, - is JobProgressEvent, - is JobResultEvent, - is JobCancelEvent, - is JobErrorEvent, - is HuddleStartedEvent, - is HuddleParticipantJoinedEvent, - is HuddleParticipantLeftEvent, - is HuddleEndedEvent, - -> consumeBuzzTimelineEvent(event, relay, wasVerified) - - // Buzz store-only regular kinds (queryable state; no timeline row yet), - // plus relay-signed sidecars and audit projections. - is ForumCommentEvent, - is ForumVoteEvent, - is StreamMessagePinnedEvent, - is StreamMessageBookmarkedEvent, - is StreamMessageScheduledEvent, - is StreamReminderEvent, - is DmCreatedEvent, - is DmOpenEvent, - is DmAddMemberEvent, - is DmHideEvent, - is MemberAddedNotificationEvent, - is AgentTurnMetricEvent, - is ModerationBanEvent, - is ModerationTimeoutEvent, - is ModerationUntimeoutEvent, - is ModerationResolveReportEvent, - is ProductFeedbackEvent, - is RelayAdminAddMemberEvent, - is RelayAdminRemoveMemberEvent, - is RelayAdminChangeRoleEvent, - is SetWorkspaceProfileEvent, - is ArchiveRequestEvent, - is UnarchiveRequestEvent, - is ArchivedIdentityEvent, - is UnarchivedIdentityEvent, - is HuddleGuidelinesEvent, - is WorkflowTriggeredEvent, - is WorkflowStepStartedEvent, - is WorkflowStepCompletedEvent, - is WorkflowStepFailedEvent, - is WorkflowCompletedEvent, - is WorkflowFailedEvent, - is WorkflowCancelledEvent, - is WorkflowApprovalRequestedEvent, - is WorkflowApprovalGrantedEvent, - is WorkflowApprovalDeniedEvent, - is WorkflowTriggerEvent, - is ApprovalGrantEvent, - is ApprovalDenyEvent, - is AuditEntryEvent, - is ChannelSummaryEvent, - is PresenceSnapshotEvent, - -> consumeBuzzRegularEvent(event, relay, wasVerified) - - // Buzz ephemeral signals: transient by definition (20000-29999) — do not - // pollute the note store, and do NOT mark the dialect from them (they are - // never stored, so the verified-mark gate has nothing to check). - is TypingIndicatorEvent -> { - // Live "who is typing" side-effect only — record the heartbeat into the - // process-wide typing state (the channel view collects it) and return - // false so it never becomes a feed row. Own-typing is filtered in the UI. - event.channelId()?.let { BuzzTypingState.record(it, event.pubKey, event.createdAt, TimeUtils.now()) } - false - } - is PresenceUpdateEvent -> { - // Live online/away/offline side-effect only — record the latest status for - // the subject (the `p` tag on relay-synthesized reads, else the author) into - // the process-wide presence state and return false so it never becomes a row. - BuzzPresenceState.record(event.subjectPubKey(), event.status(), event.createdAt) - false - } - is ObserverFrameEvent -> false - is HuddleReactionEvent -> false - // Pairing (24134) is deliberately dialect-neutral: it flows during device - // pairing before any workspace relationship is established. - is PairingEvent -> false - - // ============================================================ - // Replaceable / addressable kinds with no per-kind logic: - // the newest version per address is kept, older ones dropped. - // New kinds without custom consume logic go in one of the two - // groups below — an unlisted kind falls into the else branch - // and is rejected as unsupported. - // ============================================================ - is AcceptedBadgeSetEvent, - is AdvertisedRelayListEvent, - is AppDefinitionEvent, - is AppRecommendationEvent, - is AppSpecificDataEvent, - is AttestationEvent, - is AttestationRequestEvent, - is AttestorRecommendationEvent, - is AttestorProficiencyEvent, - is AudioTrackEvent, - is BadgeDefinitionEvent, - is BlockedRelayListEvent, - is BlossomServersEvent, - is NestsServersEvent, - is BroadcastRelayListEvent, - is BookmarkListEvent, - is OldBookmarkListEvent, - is CalendarEvent, - is CalendarDateSlotEvent, - is CalendarTimeSlotEvent, - is CalendarRSVPEvent, - is CashuWalletEvent, - is NutzapInfoEvent, - is ChannelListEvent, - is ChatMessageRelayListEvent, - is Bolt12OfferListEvent, - is ClassifiedsEvent, - is FundraiserEvent, - is BirdexEvent, - is Ps1SaveEvent, - is CommunityDefinitionEvent, - is CommunityListEvent, - is ContactListEvent, - // DVM heartbeat (11998): stored per Address(11998, author, d) so liveness checks find the - // beat at the announcement's mirror address (amethyst/plans/2026-09-10-dvm-heartbeat-liveness.md), - // AND recorded into the strong registry — beat notes are WeakReference-held with no strong - // holder on the Discover screen, so the gate must not depend on them surviving GC. - is DvmHeartbeatEvent, - -> - consumeBaseReplaceable(event, relay, wasVerified).also { - DvmHeartbeatRegistry.record(event.address(), event.createdAt) - } - is EmojiPackEvent, - is EmojiPackSelectionEvent, - is EphemeralChatListEvent, - // NIP-51 "simple groups" list (kind 10009): the user's joined NIP-29 groups + - // servers. Replaceable like its sibling lists; RelayGroupListState reads it from the - // addressable cache, so it must be stored (it was silently dropped before). - is SimpleGroupListEvent, - // Concord private joined-communities list (kind 13302). Replaceable, self-encrypted; - // ConcordChannelListState observes it via the addressable cache (Address(13302, me, "")), - // so — exactly like the 10009 list above — it must be stored replaceably or the Concord - // hub stays empty even after the event arrives. - is ConcordCommunityListEvent, - // The relay-signed NIP-29 39004 AV-participants addressable is durable group state. - is GroupParticipantsEvent, - is ExternalIdentitiesEvent, - is FileServersEvent, - is FollowListEvent, - is GeohashListEvent, - is GitRepositoryEvent, - is GitRepositoryStateEvent, - is UserGraspListEvent, - is RootSiteEvent, - is NamedSiteEvent, - is RootNappletEvent, - is NamedNappletEvent, - is RelayFeedsListEvent, - is KeyPackageEvent, - is KeyPackageRelayListEvent, - is LiveChessGameChallengeEvent, - is LiveChessGameAcceptEvent, - is LiveChessMoveEvent, - is LiveChessGameEndEvent, - is LiveChessDrawOfferEvent, - is HashtagListEvent, - is FavoriteAlgoFeedsListEvent, - is IndexerRelayListEvent, - is InteractiveStoryPrologueEvent, - is InteractiveStorySceneEvent, - is InteractiveStoryReadingStateEvent, - is InterestSetEvent, - is LabeledBookmarkListEvent, - is MeetingSpaceEvent, - is MeetingRoomEvent, - is MusicTrackEvent, - is MusicPlaylistEvent, - is AuthoredPodcastsEvent, - is FavoritePodcastsListEvent, - is Podcasting20EpisodeEvent, - is Podcasting20TrailerEvent, - is MuteListEvent, - is NNSEvent, - is PrivateOutboxRelayListEvent, - is ProfileBadgesEvent, - is ProxyRelayListEvent, - is PinListEvent, - is PeopleListEvent, - // Buzz addressable/replaceable state. - is PersonaEvent, - is TeamEvent, - is ManagedAgentEvent, - is AgentProfileEvent, - is EngramEvent, - is WorkflowDefEvent, - is EventReminderEvent, - is PushLeaseEvent, - is WindowBoundsEvent, - is ArchivedIdentitiesListEvent, - is RelayDiscoveryEvent, - is RelayMonitorEvent, - is RelaySetEvent, - is ReleaseArtifactSetEvent, - is SearchRelayListEvent, - is SoftwareApplicationEvent, - is TrustedRelayListEvent, - is TrustProviderListEvent, - is VideoHorizontalEvent, - is VideoVerticalEvent, - is WebBookmarkEvent, - is ExerciseTemplateEvent, - is PublicationIndexEvent, - is WikiRedirectEvent, - is LearningResourceEvent, - is BookshelfDirectoryEvent, - is BlossomPieceIndexEvent, - is PublicationContentEvent, - is RelayReviewEvent, - is EntityRatingEvent, - -> consumeBaseReplaceable(event, relay, wasVerified) - - // ============================================================ - // Regular kinds with no per-kind logic: stored as plain notes. - // ============================================================ - is AudioHeaderEvent, - is BadgeAwardEvent, - is CallAnswerEvent, - is CallHangupEvent, - is CallIceCandidateEvent, - is CallOfferEvent, - is CallRejectEvent, - is CallRenegotiateEvent, - is CashuTokenEvent, - is CashuSpendingHistoryEvent, - is CashuMintQuoteEvent, - // NIP-87 Cashu mint discovery + recommendations: all three are kind 3xxxx - // (parameterized-replaceable per the spec) but neither CashuMintEvent / - // FedimintEvent / MintRecommendationEvent extends AddressableEvent in Quartz - // today, so consumeBaseReplaceable's `check(event is AddressableEvent)` would - // crash. Route them as regular events — downstream consumers - // (CashuMintDirectoryState, CashuWalletState) already dedupe by (pubKey, dTag) - // and keep the newest. - is CashuMintEvent, - is FedimintEvent, - is MintRecommendationEvent, - is ChatMessageEncryptedFileHeaderEvent, - is ChatMessageEvent, - is BirdDetectionEvent, - is CommentEvent, - // The NIP-29 9xxx moderation actions and join/leave requests are regular - // one-shot events the relay is authoritative for (it applies them and - // republishes the 39000/39001/39002); we store them so they're queryable, - // but we don't act on them client-side. - is PutUserEvent, - is RemoveUserEvent, - is EditMetadataEvent, - is DeleteEventEvent, - is UpdatePinListEvent, - is DeleteGroupEvent, - is CreateGroupEvent, - is CreateInviteEvent, - is JoinRequestEvent, - is LeaveRequestEvent, - is FhirResourceEvent, - is FileHeaderEvent, - is ProfileGalleryEntryEvent, - is FileStorageHeaderEvent, - is GoalEvent, - is GroupEvent, - is GitIssueEvent, - is GitReplyEvent, - is GitPatchEvent, - is GitPullRequestEvent, - is GitPullRequestUpdateEvent, - is GitStatusEvent, - is ChessGameEvent, - is JesterEvent, - is HighlightEvent, - is PodcastEpisodeEvent, - is NIP90StatusEvent, - is NIP90ContentDiscoveryResponseEvent, - is NIP90ContentDiscoveryRequestEvent, - is NIP90UserDiscoveryResponseEvent, - is NIP90UserDiscoveryRequestEvent, - is PictureEvent, - is PrivateDmEvent, - is PublicMessageEvent, - is RequestToVanishEvent, - is CodeSnippetEvent, - is ZapPollEvent, - is RoadEventReportEvent, - is RoadEventConfirmationEvent, - is SealedRumorEvent, - is SoftwareAssetEvent, - is ExternalReactionEvent, - is ExternalCitationEvent, - is HardcopyCitationEvent, - is PromptCitationEvent, - is WikiMergeRequestEvent, - is WikiMergeAcceptanceEvent, - is TextNoteEvent, - is TorrentEvent, - is TorrentCommentEvent, - is VideoNormalEvent, - is VideoShortEvent, - is VoiceEvent, - is VoiceReplyEvent, - is WakeUpEvent, - is WelcomeEvent, - is WorkoutRecordEvent, - -> consumeRegularEvent(event, relay, wasVerified) - - else -> { - Log.w("Event Not Supported") { "From ${relay?.url}: ${event.toJson()}" }.let { false } - } - } - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.w("LocalCache", "Cannot consume ${event.toJson()} from ${relay?.url}", e) - false - } - - fun hasConsumed(notificationEvent: Event): Boolean = - if (notificationEvent is AddressableEvent) { - val note = addressables.get(notificationEvent.address()) - val noteEvent = note?.event - noteEvent != null && notificationEvent.createdAt <= noteEvent.createdAt - } else { - val note = notes.get(notificationEvent.id) - note?.event != null - } - - fun copyRelaysFromTo( - from: Note, - to: Event, - ) { - val toNote = getOrCreateNote(to) - from.relays.forEach { - toNote.addRelay(it) - } - } - - fun copyRelaysFromTo( - from: Note, - to: HexKey, - ) { - val toNote = getOrCreateNote(to) - from.relays.forEach { - toNote.addRelay(it) - } - } -} - -@Stable -class LocalCacheFlow { - 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.IO) - - // Refreshes observers in batches. - private val bundler2 = BundledInsert(1000, Dispatchers.IO) - - fun newNote(newNote: Note) { - bundler.invalidateList(newNote) { bundledNewNotes -> - _newEventBundles.emit(bundledNewNotes) - } - } - - fun removedNote(newNote: Note) { - bundler2.invalidateList(newNote) { bundledNewNotes -> - _deletedEventBundles.emit(bundledNewNotes) - } - } -} +/** + * The process-wide [EventCache]. + * + * Android reaches the cache statically from ~350 call sites, so the singleton keeps its own name + * and they need no receiver. Note that Android runs this Application class in **two** processes + * (`main` and `:napplet`) and an `object` is one copy per process — the populated cache lives only + * in `main`, and nothing reachable from the sandbox should touch this. + * + * Front ends that own their cache — Desktop, and every test that wants isolation — construct an + * [EventCache] instead. + */ +object LocalCache : EventCache() diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt index ccee686507..48c3d9b684 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.commons.model.nipBCOnchainZaps import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.OnchainZapStatus -import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.cache.EventCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nipBCOnchainZaps.verify.OnchainZapVerifier import com.vitorpamplona.quartz.nipBCOnchainZaps.verify.VerifiedOnchainZap @@ -59,7 +59,7 @@ import java.util.concurrent.ConcurrentHashMap * across cache eviction and doesn't accumulate here. */ class OnchainZapResolver( - private val cache: LocalCache, + private val cache: EventCache, ) { /** * Caps the parallelism of [reverifyOnchainZapsForNote] so a thread with many From d8347e9f8678b9d0771080c3e516e038d732682d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 16:56:54 +0000 Subject: [PATCH 04/29] refactor(desktop): follow the author for note metadata, not a cache-held index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `DesktopLocalCache` kept `notesByAuthor`, a `ConcurrentHashMap>` of every note it had ever consumed, so a kind-0 arrival could walk that author's notes and invalidate their metadata flows. Two problems with that: the map is a *strong* reference, so it quietly defeated the `LargeSoftCache` (which holds `WeakReference`s) and Desktop retained every note it ever saw for the life of the process; and it is a job the cache should not have, since a note's rendering can observe the author directly. `Event.rememberDisplayData` already existed for this, documented for exactly this case — and had no call sites. It now backs every display-data site, and the author-metadata observation inside it is extracted as `rememberAuthorMetadataKey` for the one site that renders an author without building `NoteDisplayData`. Ported: `FeedScreen` (repost and regular paths), `NoteCard` — which had hand-rolled the same `produceState` body, now deleted — and `ThreadScreen`'s reply rows, which resolved the author on the note flow. `DesktopPollCard` is a fix rather than a port: it memoized display data on the event alone and observed nothing, so a poll's author name never refreshed once rendered. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../desktop/cache/DesktopLocalCache.kt | 30 ------------------- .../amethyst/desktop/ui/EventExtensions.kt | 19 ++++++++++-- .../amethyst/desktop/ui/FeedScreen.kt | 4 +-- .../amethyst/desktop/ui/ThreadScreen.kt | 4 +++ .../desktop/ui/note/DesktopPollCard.kt | 4 +-- .../amethyst/desktop/ui/note/NoteCard.kt | 26 +++------------- 6 files changed, 29 insertions(+), 58 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt index da14a783f0..48bcea77b6 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt @@ -148,9 +148,6 @@ class DesktopLocalCache : ICacheProvider { const val MAX_CHAT_MESSAGES_PER_CHANNEL = 500 } - /** Index of notes by author pubkey — for fast metadata invalidation */ - private val notesByAuthor = ConcurrentHashMap>() - val paymentTracker = NwcPaymentTracker() /** @@ -164,13 +161,6 @@ class DesktopLocalCache : ICacheProvider { */ val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private fun trackNoteAuthor( - note: Note, - authorPubkey: HexKey, - ) { - notesByAuthor.getOrPut(authorPubkey) { ConcurrentHashMap.newKeySet() }.add(note) - } - // ----- User operations ----- override fun getUserIfExists(pubkey: HexKey): User? = users.get(pubkey) @@ -257,12 +247,6 @@ class DesktopLocalCache : ICacheProvider { if (newUserMetadata != null) { user.updateUserInfo(newUserMetadata, event) _metadataVersion.value++ - // Invalidate metadata flows on notes by this author (O(K) via index) - notesByAuthor[event.pubKey]?.forEach { note -> - if (note.flowSet?.metadata?.hasObservers() == true) { - note.flowSet?.metadata?.invalidateData() - } - } } } @@ -484,7 +468,6 @@ class DesktopLocalCache : ICacheProvider { val author = getOrCreateUser(event.pubKey) val repliesTo = event.tagsWithoutCitations().map { getOrCreateNote(it) } note.loadEvent(event, author, repliesTo) - trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } repliesTo.forEach { it.addReply(note) } addQuoteBoosts(event, note, repliesTo) @@ -504,7 +487,6 @@ class DesktopLocalCache : ICacheProvider { val author = getOrCreateUser(event.pubKey) val repliesTo = event.tagsWithoutCitations().map { getOrCreateNote(it) } note.loadEvent(event, author, repliesTo) - trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } repliesTo.forEach { it.addReply(note) } addQuoteBoosts(event, note, repliesTo) @@ -527,7 +509,6 @@ class DesktopLocalCache : ICacheProvider { if (note.event != null) return false val author = getOrCreateUser(event.pubKey) note.loadEvent(event, author, emptyList()) - trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } return true } @@ -552,7 +533,6 @@ class DesktopLocalCache : ICacheProvider { if (responseNote.event != null) return false val author = getOrCreateUser(event.pubKey) responseNote.loadEvent(event, author, emptyList()) - trackNoteAuthor(responseNote, event.pubKey) relay?.let { responseNote.addRelay(it) } pollNote.pollState().addResponse(responseNote) return true @@ -598,7 +578,6 @@ class DesktopLocalCache : ICacheProvider { event.originalPost().mapNotNull { getNoteIfExists(it) } + event.taggedAddresses().mapNotNull { addressableNotes.get(it.toValue()) } note.loadEvent(event, author, reactedTo) - trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } reactedTo.forEach { it.addReaction(note) } return true @@ -616,7 +595,6 @@ class DesktopLocalCache : ICacheProvider { if (note.event != null) return false val author = getOrCreateUser(event.pubKey) note.loadEvent(event, author, emptyList()) - trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } return true } @@ -648,7 +626,6 @@ class DesktopLocalCache : ICacheProvider { event.taggedAddresses().mapNotNull { addressableNotes.get(it.toValue()) } note.loadEvent(event, author, zappedNotes) - trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } // Link zap to target notes @@ -676,7 +653,6 @@ class DesktopLocalCache : ICacheProvider { val boostedNote = boostedId?.let { getOrCreateNote(it) } val repliesTo = listOfNotNull(boostedNote) note.loadEvent(event, author, repliesTo) - trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } boostedNote?.addBoost(note) return true @@ -696,7 +672,6 @@ class DesktopLocalCache : ICacheProvider { val boostedNote = event.boostedEventId()?.let { getOrCreateNote(it) } val repliesTo = listOfNotNull(boostedNote) note.loadEvent(event, author, repliesTo) - trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } boostedNote?.addBoost(note) return true @@ -770,7 +745,6 @@ class DesktopLocalCache : ICacheProvider { if (note.event != null) return false val author = getOrCreateUser(event.pubKey) note.loadEvent(event, author, emptyList()) - trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } return true } @@ -884,7 +858,6 @@ class DesktopLocalCache : ICacheProvider { val author = getOrCreateUser(event.pubKey) note.loadEvent(event, author, emptyList()) - trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } val activityAddress = event.activityAddress() ?: return true @@ -953,7 +926,6 @@ class DesktopLocalCache : ICacheProvider { if (note.event != null) return false note.loadEvent(event, author, emptyList()) - trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } zappedNote?.addZapPayment(note, null) @@ -999,7 +971,6 @@ class DesktopLocalCache : ICacheProvider { if (note.event != null) return false note.loadEvent(event, author, emptyList()) - trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } // Link response to zapped note via request @@ -1140,7 +1111,6 @@ class DesktopLocalCache : ICacheProvider { _followedUsers.value = emptySet() followerCounts.clear() followingCounts.clear() - notesByAuthor.clear() lastContactListByAuthor.clear() lastContactListEvent = null accountPubkey = null diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/EventExtensions.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/EventExtensions.kt index c580f863c7..34529ce8f2 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/EventExtensions.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/EventExtensions.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.desktop.ui.note.NoteDisplayData import com.vitorpamplona.quartz.nip01Core.core.Event @@ -41,7 +42,21 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub @Composable fun Event.rememberDisplayData(cache: ICacheProvider?): NoteDisplayData { val author = remember(pubKey, cache) { cache?.getUserIfExists(pubKey) } - val authorMetaValue by produceState(initialValue = null, key1 = author) { + val authorMetaValue = rememberAuthorMetadataKey(author) + return remember(this, authorMetaValue) { toNoteDisplayData(cache) } +} + +/** + * A token that changes whenever [author]'s kind-0 metadata arrives. + * + * Use it as a `remember` key wherever rendering reads an author's name or avatar. A note is + * almost always cached before its author's profile is, so a one-time lookup draws a pubkey stub + * and never corrects itself — the cache holds no note-by-author index to invalidate from, and + * should not: that index would strongly retain every note it knows. + */ +@Composable +fun rememberAuthorMetadataKey(author: User?): Any? { + val metaValue by produceState(initialValue = null, key1 = author) { val a = author if (a == null) { value = null @@ -49,7 +64,7 @@ fun Event.rememberDisplayData(cache: ICacheProvider?): NoteDisplayData { a.metadata().flow.collect { value = it } } } - return remember(this, authorMetaValue) { toNoteDisplayData(cache) } + return metaValue } /** diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index 9ae8b96117..e6ad78c4cc 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -377,7 +377,7 @@ private fun FeedNoteCardBody( } // Original note content with actions inside card - val displayData = remember(originalEvent, metadataState) { originalEvent.toNoteDisplayData(localCache) } + val displayData = originalEvent.rememberDisplayData(localCache) val showRepostFollowPill = account != null && onFollow != null && @@ -449,7 +449,7 @@ private fun FeedNoteCardBody( onDispose { note.clearFlow() } } - val displayData = remember(event, metadataState) { event.toNoteDisplayData(localCache) } + val displayData = event.rememberDisplayData(localCache) val showFollowPill = account != null && onFollow != null && diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt index 69662213e8..dfb5809e63 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt @@ -385,6 +385,10 @@ fun ThreadScreen( event?.pubKey?.let { localCache.getUserIfExists(it) } } + // Read so this row recomposes when the reply author's + // kind-0 arrives — usually well after the reply does. + rememberAuthorMetadataKey(author) + val reactionCount = remember(reactionsState) { note.countReactions() } val zapAmount = remember(zapsState) { note.zapsAmount } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopPollCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopPollCard.kt index 7a9dc76585..b668e4ddac 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopPollCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopPollCard.kt @@ -78,7 +78,7 @@ import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.amethyst.desktop.subscriptions.SubscriptionConfig import com.vitorpamplona.amethyst.desktop.subscriptions.generateSubId import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription -import com.vitorpamplona.amethyst.desktop.ui.toNoteDisplayData +import com.vitorpamplona.amethyst.desktop.ui.rememberDisplayData import com.vitorpamplona.amethyst.desktop.ui.voteOnPoll import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent @@ -206,7 +206,7 @@ fun DesktopPollCard( endsAtSec?.let { (if (hasEnded) "Ended " else "Ends ") + formatPollTimestamp(it) } } - val displayData = remember(event) { event.toNoteDisplayData(localCache) } + val displayData = event.rememberDisplayData(localCache) NoteCard( note = displayData, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt index 54b5971176..0ab4912125 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt @@ -44,7 +44,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState 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 @@ -72,7 +71,7 @@ import com.vitorpamplona.amethyst.desktop.ui.media.DesktopVideoPlayer import com.vitorpamplona.amethyst.desktop.ui.media.LocalWindowState import com.vitorpamplona.amethyst.desktop.ui.media.isAnimatedGifUrl import com.vitorpamplona.amethyst.desktop.ui.note.WoTBadgedAvatar -import com.vitorpamplona.amethyst.desktop.ui.toNoteDisplayData +import com.vitorpamplona.amethyst.desktop.ui.rememberDisplayData import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NNote @@ -529,28 +528,11 @@ fun QuotedNoteEmbed( onDispose { note.clearFlow() } } - // Observe the author's user metadata (kind 0) so the embed's name + avatar - // update once metadata arrives via relay subscription. produceState avoids - // the conditional-composable trap when note.author is null until the parent - // event lands. - val author = note.author - val authorMetaValue by produceState(initialValue = null, key1 = author) { - val a = author - if (a == null) { - value = null - } else { - a.metadata().flow.collect { value = it } - } - } - val event = note.event if (event != null) { - // Recompute on every recomposition — picks up note + user metadata changes. - // authorMetaValue is read to mark this recomposition path as author-meta - // dependent so toNoteDisplayData() sees the latest avatar/displayName. - @Suppress("UNUSED_EXPRESSION") - authorMetaValue - val displayData = event.toNoteDisplayData(localCache) + // Re-derives when the author's kind-0 lands, so the embed's name and avatar fill in + // after the note itself. + val displayData = event.rememberDisplayData(localCache) SpamCheckedNoteRender( note = note, From f8309bd72144c1cc9ac95482237efeb25ddf3bb0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 17:00:52 +0000 Subject: [PATCH 05/29] docs(commons): record the part B compatibility analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What is already compatible between the two caches, what part A resolved, and the shape the rest should take — a facade over an owned `EventCache` rather than a deletion, with storage unified first and routing swapped kind by kind. Also records the two behaviour changes to watch for (kind-0 starting to persist through `LocalRelayStore`, notes outside a loaded feed becoming GC-eligible) and that the 62 desktop cache tests do not cover a stale render, so the app needs a run against a real relay before part B is called done. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../2026-08-30-commons-migration-sweep.md | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index 4d8512e845..043c23fd6f 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -920,3 +920,83 @@ and `RailCapability` use it, `OnchainZapResolver` does not. `:desktopApp:compileKotlin`, `:cli:compileKotlin`, `:commons:verifyKmpPurity`, `:commons:jvmTest`, `:cli:test`, `:amethyst:testPlayDebugUnitTest`, `spotlessApply`. + + +### Wave 2 part B — compatibility analysis and the road to deleting `DesktopLocalCache` (2026-09-19) + +Part A moved the cache. Part B is retiring the Desktop fork. The naive framing +("repoint ~70 consumers and delete 1,177 lines") is wrong; what follows is the +measured picture. + +#### What is already compatible + +- **`consume()` coverage is a strict superset.** All 22 kinds Desktop routes + are handled; the 9 without a dedicated `EventCache` overload + (`TextNoteEvent`, `ContactListEvent`, `CommentEvent`, + `AdvertisedRelayListEvent`, `BlossomServersEvent`, `BookmarkListEvent`, + `OldBookmarkListEvent`, `ChatMessageRelayListEvent`, `FollowListEvent`) fall + into the generic replaceable/addressable group. +- **13 of 13 core read methods match** by name and signature. +- **Feed retention is already aligned** — both platforms hold feed content + strongly in the shared `FeedContentState`. +- **The event stream is separable.** Desktop's `newEventBundles` is driven by + `DesktopRelaySubscriptionsCoordinator`, not by the cache, so Desktop can keep + owning `DesktopCacheEventStream` and does not inherit `LocalCacheFlow`'s + 1 s `BundledInsert` window. + +#### Resolved during part B + +- **`notesByAuthor`** — a strong `ConcurrentHashMap>` + of every note ever consumed, which defeated the `LargeSoftCache` + (`WeakReference`, despite the name) and made Desktop retain every note for + the life of the process. It also drove kind-0 metadata invalidation, which + Android has no counterpart for. Removed: every display-data site now observes + the author's `User` metadata flow through `Event.rememberDisplayData` (which + already existed, documented for this, with zero call sites). +- **`object` vs `class`** — `EventCache` is now the class, `LocalCache` the + process-wide `object` over it. Android's ~350 static call sites were untouched. +- **`findUsersStartingWith(prefix, limit)`** — `EventCache` now overrides the + port method instead of inheriting its `emptyList()` default. + +#### Still to do, and the shape it should take + +`DesktopLocalCache` should become a **facade over an owned `EventCache`**, not +a deletion: ~500 lines of genuinely Desktop-specific state survive, and the +~700 lines of `consumeXxx` routing go. + +1. **Storage unification first** (safe, mechanical): drop Desktop's `users` / + `notes` / `addressableNotes` / `liveChatChannels` and delegate to the owned + `EventCache`. Desktop's own `consumeXxx` methods keep working because they + go through `getOrCreateNote` / `getOrCreateAddressableNote`. Note the key + type widens (`addressableNotes` → `addressables`); + the one external reader (`DesktopRelaySubscriptionsCoordinator:599`) ignores + the key. +2. **Then swap routing kind by kind**, each step green against the 62 tests in + the 8 `desktop/cache` test classes, ending at + `cache.checkDeletionAndConsume(event, relay, true)`. +3. **Desktop-only state stays**, moved behind the facade: `localRelayStore` + write-through, `followedUsers`, `accountPubkey`, `contactListEvents`, + `metadataVersion`, `followPackVersion` / `liveActivityVersion`, the + snapshots, `cachedAdvertisedRelayList`, follower/following counts, + `onProfileMetadataConsumed`, `appScope`, `eventStream`. The mutations are + concentrated in five places (metadata, contact list, follow pack, live + activity, `clear`) and re-derive cleanly from the event after consume. +4. **Two Desktop-shaped `consume` overloads do not generalize** — the NIP-47 + `LnZapPaymentRequestEvent` one takes a `zappedNote` and an `onResponse` + callback, and the response one drives `paymentTracker` + `appScope`. Android + reaches the same tracker through account state. Keep them Desktop-side. +5. **`clear()`** has only 2 production call sites (`Main.kt`, logout/account + switch) plus 2 in tests. `EventCache` deliberately has no `clear()`: + `DeletionIndex`, `FilterIndex`, `HintIndexer` and `NwcPaymentTracker` have + no way to reset, so one would be a half-truth. Construct a fresh + `EventCache` instead — which is what making it a class bought. + +**Known behaviour changes to watch for when this lands:** kind-0 currently +returns `false` from Desktop's `route()` and is therefore never written through +to `LocalRelayStore`; `EventCache` returns `true` when the metadata updated, so +profiles would start persisting. And notes outside a loaded feed become +GC-eligible on Desktop, as they already are on Android. + +**The test suite is not sufficient for this step.** 62 tests cover the consume +path well, but nothing covers a stale render. Run the desktop app against a +real relay before calling part B done. From 18b217c5a9eab4ca7a5c0ff9ea3ca2875b23d0f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 23:50:31 +0000 Subject: [PATCH 06/29] fix(desktop): bind the thread reply's name and avatar to the metadata key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The observation was a bare `rememberAuthorMetadataKey(author)` statement. It works — a composable returning a value is not restartable, so the state read invalidates the caller's scope — but it reads as a no-op and would not survive the first person tidying the file. The name and avatar now derive from the key they actually depend on. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../amethyst/desktop/ui/ThreadScreen.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt index dfb5809e63..044719f35a 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt @@ -385,9 +385,12 @@ fun ThreadScreen( event?.pubKey?.let { localCache.getUserIfExists(it) } } - // Read so this row recomposes when the reply author's - // kind-0 arrives — usually well after the reply does. - rememberAuthorMetadataKey(author) + // The reply almost always renders before its author's + // kind-0 does, so the name and avatar have to come from + // an observation rather than the lookup above. + val authorMeta = rememberAuthorMetadataKey(author) + val authorName = remember(author, authorMeta) { author?.toBestDisplayName() } + val authorAvatar = remember(author, authorMeta) { author?.profilePicture() } val reactionCount = remember(reactionsState) { note.countReactions() } @@ -395,13 +398,13 @@ fun ThreadScreen( CommentItem( authorName = - author?.toBestDisplayName() + authorName ?: event?.pubKey?.take(8) ?: "", authorHandle = author?.pubkeyNpub()?.take(16)?.let { "@$it..." } ?: "", - authorAvatarUrl = author?.profilePicture(), + authorAvatarUrl = authorAvatar, authorPubKeyHex = event?.pubKey ?: "", content = event?.content ?: "", timeAgo = (event?.createdAt ?: 0L).toTimeAgo(), From 19a0cb082a31284edd0393441fea40e909e5b702 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 23:52:51 +0000 Subject: [PATCH 07/29] style(commons): drop the now-redundant qualified names in getEventStream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ICacheEventStream` was written out in full when the cache lived in the app module. It is the same package now, so the simple name resolves — and the qualified form is what the project's Kotlin style rule forbids. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../vitorpamplona/amethyst/commons/model/cache/EventCache.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt index 5028446fe3..b1a59820f6 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt @@ -868,8 +868,8 @@ open class EventCache : return null } - override fun getEventStream(): com.vitorpamplona.amethyst.commons.model.cache.ICacheEventStream = - object : com.vitorpamplona.amethyst.commons.model.cache.ICacheEventStream { + override fun getEventStream(): ICacheEventStream = + object : ICacheEventStream { override val newEventBundles = live.newEventBundles override val deletedEventBundles = live.deletedEventBundles } From 339819928a99f0ca84997f544288e0c3d1d4219a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 18:18:15 +0000 Subject: [PATCH 08/29] fix(nav): land on the tab root when a nav-bar item is tapped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On tablets the navigation rail stays on screen while the user is deep inside a tab, so — unlike the phone bottom bar, which hides itself off tab roots — it can be tapped from anywhere. Tapping Home from there came back to whatever screen the user had left open rather than to the feed. `popUpTo(Home) { inclusive = false; saveState = true }` files the popped entries under the popUpTo target's own destination id as well as the popped tab's (the `if (!inclusive)` branch of NavControllerImpl.executePopOperations), so the following `navigate(Home) { restoreState = true }` handed Home back the stack that was saved on the way out of some other tab. navBottomBar now does two things before the navigate: - drops whatever the user pushed on top of the tab root they are in, without saving it, so no deep stack is ever eligible for a later restore; - pops back to the tab when it is already on the stack — always true for Home, and for the tab the user is inside — instead of navigating to it. The tab root keeps the ViewModelStore and scroll position it already has, and Home never goes through restoreState at all. Phones are unaffected: the bottom bar is only reachable from a tab root, so there is nothing to pop and the net stack is the same as before. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014PdeozanQPzJ5kzFTDhMnZ --- .../amethyst/ui/navigation/navs/Nav.kt | 48 ++++++++ .../ui/navigation/NavBottomBarStackTest.kt | 107 ++++++++++++++++++ .../ui/navigation/NavImeSettleTest.kt | 9 ++ 3 files changed, 164 insertions(+) create mode 100644 amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavBottomBarStackTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt index ac0adde7bc..652e515fb9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import com.vitorpamplona.amethyst.ui.navigation.BOTTOM_NAV_ROOT_KEY @@ -102,6 +103,34 @@ class Nav( override fun navBottomBar(route: Route) { navigationScope.launch { ime.settle() + + // A nav-bar tap asks for a tab, never for whatever the user pushed on top of one. Drop + // those pushes first, and without saving them, so the restoreState below can never hand + // a deep stack back. On phones this is always a no-op — AppBottomBar hides itself off + // tab roots, so the bar is only ever tapped from one — but the large-screen rail stays + // on screen the whole time and is routinely tapped from three screens deep. + popPushesAboveTabRoot() + + // Home always sits at the bottom of the stack (it is the graph's start destination and + // the anchor of the popUpTo below), and so does the tab the user is already inside: + // popping back to it beats navigating to it. The tab root keeps the ViewModelStore and + // scroll position it already has, and the branch above it is saved under its own tab, + // exactly as the navigate path would have saved it. + // + // Home in particular MUST come back this way. `popUpTo(Home) { inclusive = false; + // saveState = true }` files the popped entries under the popUpTo target's own + // destination id as well as the popped tab's — the `if (!inclusive)` branch of + // NavControllerImpl.executePopOperations — so `navigate(Home) { restoreState = true }` + // hands Home back whatever was popped on the way out of some *other* tab. That is how + // tapping Home on the rail came back to the thread the user had left open three screens + // deep in another tab instead of to the home feed. + val existing = runCatching { controller.getBackStackEntry(route) }.getOrNull() + if (existing != null) { + controller.popBackStack(route, inclusive = false, saveState = true) + existing.savedStateHandle.set(BOTTOM_NAV_ROOT_KEY, true) + return@launch + } + controller.navigate(route) { // Clear sibling bottom-nav entries but keep Home (the start // destination) below, so back-swipe from any tab returns to @@ -146,6 +175,25 @@ class Nav( } } + /** + * Drops every entry the user pushed on top of the tab root they are currently in, discarding + * that state rather than saving it — a nav-bar tap is a request for a tab, and nothing above a + * tab root should survive to be replayed by a later `restoreState`. + * + * Stops at the first entry stamped [BOTTOM_NAV_ROOT_KEY] by [navBottomBar], or at Home, which + * is a tab root the user may never have tapped because the graph starts there. Terminates + * either way: every iteration that does not return removes one entry from a finite back stack, + * and [NavHostController.popBackStack] reports false once there is nothing left to pop. + */ + private fun popPushesAboveTabRoot() { + while (true) { + val top = controller.currentBackStackEntry ?: return + if (top.isBottomNavRoot()) return + if (top.destination.hasRoute(Route.Home::class)) return + if (!controller.popBackStack()) return + } + } + @Composable override fun canPop(): Boolean { // Decide the back arrow / bottom-bar visibility from THIS screen's own diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavBottomBarStackTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavBottomBarStackTest.kt new file mode 100644 index 0000000000..2d8150cac5 --- /dev/null +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavBottomBarStackTest.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.navigation + +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavHostController +import androidx.navigation.NavOptionsBuilder +import com.vitorpamplona.amethyst.ui.navigation.navs.Nav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** + * A nav-bar tap must land on the tab itself, never on whatever the user had pushed on top of one. + * + * The phone bottom bar gets this for free — it hides itself off tab roots, so it can only ever be + * tapped from one. The large-screen navigation rail stays on screen the whole time, which is where + * the gap showed: tapping Home from a thread three screens deep came back to that thread instead of + * the feed. + * + * Two things had to be true for that, and each test below pins one of them down: + * + * 1. Pushes above a tab root are dropped, unsaved, on the way out. Anything saved is eligible to be + * replayed by a later `restoreState`. + * 2. A tab that is already on the back stack — always the case for Home, which is the graph's start + * destination — is reached by popping back to it, not by navigating to it. + * + * (2) is what actually broke. `popUpTo(Home) { inclusive = false; saveState = true }` files the + * popped entries under the popUpTo target's own destination id as well as the popped tab's — see + * the `if (!inclusive)` branch of `NavControllerImpl.executePopOperations` — so a later + * `navigate(Home) { restoreState = true }` handed Home back the stack the user had left behind in + * some *other* tab. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class NavBottomBarStackTest { + /** A back-stack entry that is or isn't a tab root, as far as [isBottomNavRoot] can tell. */ + private fun entry(isTabRoot: Boolean): NavBackStackEntry = + mockk(relaxed = true) { + every { savedStateHandle.get(BOTTOM_NAV_ROOT_KEY) } returns isTabRoot + } + + @Test + fun dropsWhatTheUserPushedOnTopOfTheTabBeforeSwitchingTabs() = + runTest { + // Home > Note > Profile, with Home stamped as the tab root the two pushes sit on. + val controller = + mockk(relaxed = true) { + every { currentBackStackEntry } returnsMany + listOf(entry(isTabRoot = false), entry(isTabRoot = false), entry(isTabRoot = true)) + every { popBackStack() } returns true + // Messages is not on the stack until the navigate below puts it there. + every { getBackStackEntry(Route.Message) } throws + IllegalArgumentException("No destination is on the NavController's back stack") andThen + entry(isTabRoot = true) + } + + Nav(controller, this).navBottomBar(Route.Message) + advanceUntilIdle() + + // Exactly the two pushes, and no more: the tab root itself stays, keeping its + // ViewModelStore for the tab's own restore. + verify(exactly = 2) { controller.popBackStack() } + verify(exactly = 1) { controller.navigate(Route.Message, any Unit>()) } + } + + @Test + fun popsBackToATabThatIsAlreadyOnTheStackInsteadOfNavigatingToIt() = + runTest { + val home = entry(isTabRoot = true) + val controller = + mockk(relaxed = true) { + every { currentBackStackEntry } returns home + every { getBackStackEntry(Route.Home) } returns home + } + + Nav(controller, this).navBottomBar(Route.Home) + advanceUntilIdle() + + // Popping back to Home keeps the feed's own state and, unlike navigating, cannot pick up + // a saved stack that popUpTo(Home) filed under Home's destination id. + verify(exactly = 1) { controller.popBackStack(Route.Home, inclusive = false, saveState = true) } + verify(exactly = 0) { controller.navigate(any(), any Unit>()) } + } +} diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavImeSettleTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavImeSettleTest.kt index 79d95b500b..878810bfcc 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavImeSettleTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavImeSettleTest.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.navigation +import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController import androidx.navigation.NavOptionsBuilder import com.vitorpamplona.amethyst.ui.navigation.navs.ImeSettler @@ -50,6 +51,14 @@ import org.junit.Test class NavImeSettleTest { private fun controllerRecording(order: MutableList): NavHostController = mockk(relaxed = true) { + // An empty back stack: nothing is pushed over a tab root and no tab is already on it, + // so navBottomBar takes its navigate path rather than the pop-back-to-the-tab shortcut. + // The tab lands on the stack once that navigate runs, which is what stops navBottomBar + // from falling through to its second, sibling-destination navigate. + every { popBackStack() } returns false + every { getBackStackEntry(any()) } throws + IllegalArgumentException("No destination is on the NavController's back stack") andThen + mockk(relaxed = true) every { navigate(any(), any Unit>()) } answers { order.add("navigate") } every { navigate(any()) } answers { order.add("navigate") } From 49fad0c619e982f74a13a4fb631b815af167b2f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 21:21:00 +0000 Subject: [PATCH 09/29] refactor(nav): fold the tab-tap fix into one navigate path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first cut grew navBottomBar to three exits: drop the pushes, pop back to the tab when it was already on the stack, or navigate. The middle one was doing by hand what the navigate already does. Once the pushes are gone the stack is [Home] or [Home, tab], so the only thing the navigate gets wrong is Home — the popUpTo anchor, whose own destination id a non-inclusive saveState pop also files the popped entries under. Saying so directly, `restoreState = route != Route.Home`, replaces the whole branch; the already-there case falls out of the same guard nav() uses, and Home keeps its ViewModelStore through launchSingleTop rather than through a pop. The tests now assert the contract (the NavOptions the tap builds) rather than which controller method was called, and NavImeSettleTest needs no stubs again. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014PdeozanQPzJ5kzFTDhMnZ --- .../amethyst/ui/navigation/navs/Nav.kt | 32 ++++---- .../ui/navigation/NavBottomBarStackTest.kt | 75 +++++++++++-------- .../ui/navigation/NavImeSettleTest.kt | 9 --- 3 files changed, 56 insertions(+), 60 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt index 652e515fb9..53f02d6c7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt @@ -111,23 +111,11 @@ class Nav( // on screen the whole time and is routinely tapped from three screens deep. popPushesAboveTabRoot() - // Home always sits at the bottom of the stack (it is the graph's start destination and - // the anchor of the popUpTo below), and so does the tab the user is already inside: - // popping back to it beats navigating to it. The tab root keeps the ViewModelStore and - // scroll position it already has, and the branch above it is saved under its own tab, - // exactly as the navigate path would have saved it. - // - // Home in particular MUST come back this way. `popUpTo(Home) { inclusive = false; - // saveState = true }` files the popped entries under the popUpTo target's own - // destination id as well as the popped tab's — the `if (!inclusive)` branch of - // NavControllerImpl.executePopOperations — so `navigate(Home) { restoreState = true }` - // hands Home back whatever was popped on the way out of some *other* tab. That is how - // tapping Home on the rail came back to the thread the user had left open three screens - // deep in another tab instead of to the home feed. - val existing = runCatching { controller.getBackStackEntry(route) }.getOrNull() - if (existing != null) { - controller.popBackStack(route, inclusive = false, saveState = true) - existing.savedStateHandle.set(BOTTOM_NAV_ROOT_KEY, true) + // Dropping those pushes is often the whole job — re-tapping the tab the user is inside, + // and every tap of Home from somewhere inside Home, end here. Same already-there guard + // nav() uses, so a parameterized tab compares by its filled route. + if (getRouteWithArguments(route::class, controller) == route) { + controller.currentBackStackEntry?.savedStateHandle?.set(BOTTOM_NAV_ROOT_KEY, true) return@launch } @@ -146,7 +134,15 @@ class Nav( saveState = true } launchSingleTop = true - restoreState = true + // ...but never onto Home. A non-inclusive popUpTo files the popped entries under the + // popUpTo TARGET's own destination id as well as the popped tab's — the + // `if (!inclusive)` branch of NavControllerImpl.executePopOperations — so restoring + // here would hand Home the stack that was saved on the way out of some *other* tab. + // That is how tapping Home on the rail came back to a thread instead of the feed. + // Home is the anchor, so it is never popped and has no saved stack of its own to + // miss: launchSingleTop reuses the entry already sitting there, ViewModelStore and + // all. + restoreState = route != Route.Home } // Mark this entry as a tab root: hides the back arrow in canPop // and skips the horizontal slide in composableFromEnd. diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavBottomBarStackTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavBottomBarStackTest.kt index 2d8150cac5..c5cac66bbb 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavBottomBarStackTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavBottomBarStackTest.kt @@ -22,15 +22,21 @@ package com.vitorpamplona.amethyst.ui.navigation import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController +import androidx.navigation.NavOptions import androidx.navigation.NavOptionsBuilder +import androidx.navigation.navOptions import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import io.mockk.every import io.mockk.mockk +import io.mockk.slot import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test /** @@ -41,67 +47,70 @@ import org.junit.Test * the gap showed: tapping Home from a thread three screens deep came back to that thread instead of * the feed. * - * Two things had to be true for that, and each test below pins one of them down: + * Two rules keep that from happening, one per test below: * - * 1. Pushes above a tab root are dropped, unsaved, on the way out. Anything saved is eligible to be + * 1. Pushes above a tab root are dropped, unsaved, on the way out — anything saved is eligible to be * replayed by a later `restoreState`. - * 2. A tab that is already on the back stack — always the case for Home, which is the graph's start - * destination — is reached by popping back to it, not by navigating to it. - * - * (2) is what actually broke. `popUpTo(Home) { inclusive = false; saveState = true }` files the - * popped entries under the popUpTo target's own destination id as well as the popped tab's — see - * the `if (!inclusive)` branch of `NavControllerImpl.executePopOperations` — so a later - * `navigate(Home) { restoreState = true }` handed Home back the stack the user had left behind in - * some *other* tab. + * 2. Home is never restored onto. `popUpTo(Home) { inclusive = false; saveState = true }` files the + * popped entries under the popUpTo target's own destination id as well as the popped tab's — see + * the `if (!inclusive)` branch of `NavControllerImpl.executePopOperations` — so + * `navigate(Home) { restoreState = true }` handed Home back the stack the user had left behind in + * some *other* tab. Every other tab restores as before; that is what keeps its ViewModelStore. */ @OptIn(ExperimentalCoroutinesApi::class) class NavBottomBarStackTest { - /** A back-stack entry that is or isn't a tab root, as far as [isBottomNavRoot] can tell. */ + /** A back-stack entry that is, or isn't, a tab root as far as [isBottomNavRoot] can tell. */ private fun entry(isTabRoot: Boolean): NavBackStackEntry = mockk(relaxed = true) { every { savedStateHandle.get(BOTTOM_NAV_ROOT_KEY) } returns isTabRoot } + /** + * Taps [route] on a controller whose back stack holds none of the nav-bar destinations, and + * returns the options the resulting navigate was built with. + */ + private fun TestScope.optionsForTapping(route: Route): NavOptions { + val options = slot Unit>() + val controller = + mockk(relaxed = true) { + every { currentBackStackEntry } returns entry(isTabRoot = true) + every { navigate(route, capture(options)) } returns Unit + } + + Nav(controller, this).navBottomBar(route) + advanceUntilIdle() + + return navOptions(options.captured) + } + @Test fun dropsWhatTheUserPushedOnTopOfTheTabBeforeSwitchingTabs() = runTest { - // Home > Note > Profile, with Home stamped as the tab root the two pushes sit on. + // Home > Note > Profile: two pushes sitting on a tab root. val controller = mockk(relaxed = true) { every { currentBackStackEntry } returnsMany listOf(entry(isTabRoot = false), entry(isTabRoot = false), entry(isTabRoot = true)) every { popBackStack() } returns true - // Messages is not on the stack until the navigate below puts it there. - every { getBackStackEntry(Route.Message) } throws - IllegalArgumentException("No destination is on the NavController's back stack") andThen - entry(isTabRoot = true) } Nav(controller, this).navBottomBar(Route.Message) advanceUntilIdle() - // Exactly the two pushes, and no more: the tab root itself stays, keeping its - // ViewModelStore for the tab's own restore. + // Exactly the two pushes and no more: the tab root itself stays, so the navigate below + // saves a tab root rather than a branch that could be replayed later. verify(exactly = 2) { controller.popBackStack() } - verify(exactly = 1) { controller.navigate(Route.Message, any Unit>()) } } @Test - fun popsBackToATabThatIsAlreadyOnTheStackInsteadOfNavigatingToIt() = + fun neverRestoresASavedStackOntoHome() = runTest { - val home = entry(isTabRoot = true) - val controller = - mockk(relaxed = true) { - every { currentBackStackEntry } returns home - every { getBackStackEntry(Route.Home) } returns home - } + assertFalse(optionsForTapping(Route.Home).shouldRestoreState()) + } - Nav(controller, this).navBottomBar(Route.Home) - advanceUntilIdle() - - // Popping back to Home keeps the feed's own state and, unlike navigating, cannot pick up - // a saved stack that popUpTo(Home) filed under Home's destination id. - verify(exactly = 1) { controller.popBackStack(Route.Home, inclusive = false, saveState = true) } - verify(exactly = 0) { controller.navigate(any(), any Unit>()) } + @Test + fun restoresTheSavedStackOfEveryOtherTab() = + runTest { + assertTrue(optionsForTapping(Route.Message).shouldRestoreState()) } } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavImeSettleTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavImeSettleTest.kt index 878810bfcc..79d95b500b 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavImeSettleTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/navigation/NavImeSettleTest.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.navigation -import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController import androidx.navigation.NavOptionsBuilder import com.vitorpamplona.amethyst.ui.navigation.navs.ImeSettler @@ -51,14 +50,6 @@ import org.junit.Test class NavImeSettleTest { private fun controllerRecording(order: MutableList): NavHostController = mockk(relaxed = true) { - // An empty back stack: nothing is pushed over a tab root and no tab is already on it, - // so navBottomBar takes its navigate path rather than the pop-back-to-the-tab shortcut. - // The tab lands on the stack once that navigate runs, which is what stops navBottomBar - // from falling through to its second, sibling-destination navigate. - every { popBackStack() } returns false - every { getBackStackEntry(any()) } throws - IllegalArgumentException("No destination is on the NavController's back stack") andThen - mockk(relaxed = true) every { navigate(any(), any Unit>()) } answers { order.add("navigate") } every { navigate(any()) } answers { order.add("navigate") } From 5165dc3110f00aa8ba376cf808d8d8c70a32ae27 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 01:32:03 +0000 Subject: [PATCH 10/29] fix(desktop): request the profiles of thread reply authors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reply rows in the Desktop thread view showed a truncated pubkey and a blank avatar forever. Nothing ever asked for those profiles. `NoteCard` resolves its author with `getOrCreateUser` and opens a composition-scoped `UserFinderFilterAssemblerSubscription`, which is what puts a kind-0 REQ on the wire while a card is on screen. Reply rows do not render through `NoteCard` — they render through `CommentItem`, which takes plain strings and opens nothing — and they resolved the author with `getUserIfExists`, which answers with a `User` carrying no metadata (or with null for a stranger the cache has never seen). The comment above the row loop asserted the opposite, that "each thread note renders through NoteCard"; it was wrong for exactly these rows, and is corrected. The row now resolves its author with `getOrCreateUser` and opens the same subscription `NoteCard` does, so the profile is requested and the existing metadata observation fills the name and avatar in when it lands. Two related fixes while here: - `Event.rememberDisplayData` had the same `getUserIfExists` weakness: a note is routinely cached before its author's profile is, and a null author left nothing to observe, so the name could never correct itself. It now get-or-creates too. - The row read `note.event` bare, above an unread `collectAsState`. A `State` that composition never reads invalidates nothing, so a reply that arrived as a placeholder would not have re-rendered when its event landed. The read is now keyed on the metadata slot that `Note.loadEvent` invalidates. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../amethyst/desktop/ui/EventExtensions.kt | 5 ++- .../amethyst/desktop/ui/ThreadScreen.kt | 38 ++++++++++++++----- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/EventExtensions.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/EventExtensions.kt index 34529ce8f2..058c6567c1 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/EventExtensions.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/EventExtensions.kt @@ -41,7 +41,10 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub */ @Composable fun Event.rememberDisplayData(cache: ICacheProvider?): NoteDisplayData { - val author = remember(pubKey, cache) { cache?.getUserIfExists(pubKey) } + // getOrCreate, not getIfExists: a note is routinely cached before its author's profile is, + // and a null author here would leave nothing to observe, so the name would never correct + // itself from the pubkey stub. + val author = remember(pubKey, cache) { cache?.getOrCreateUser(pubKey) } val authorMetaValue = rememberAuthorMetadataKey(author) return remember(this, authorMetaValue) { toNoteDisplayData(cache) } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt index 044719f35a..afcb78f082 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction +import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.richtext.UrlParser import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.components.LoadingState @@ -193,9 +194,10 @@ fun ThreadScreen( onDispose { subId?.let { coordinator.releaseInteractions(it) } } } - // Thread-author metadata + note interactions now load per row: each thread - // note renders through NoteCard, which opens composition-scoped UserFinder + - // EventFinder subscriptions. (requestInteractions above remains the thread's + // Thread-author metadata + note interactions load per row, through the + // composition-scoped UserFinder + EventFinder subscriptions that NoteCard + // opens. Reply rows render through CommentItem instead, so they open their + // own UserFinder below. (requestInteractions above remains the thread's // explicit interaction-refresh path.) // Fetch quoted notes referenced in thread content @@ -369,22 +371,40 @@ fun ThreadScreen( ) } else { replyNotes.forEachIndexed { index, note -> - val event = note.event - - // Observe metadata + reactions so we recompose - // when author info arrives from relays + // Observe this note's own flows so the row recomposes + // when its event, reactions or zaps land. The author's + // profile is a separate observation, further down. val flowSet = remember(note) { note.flow() } val metadataState by flowSet.metadata.stateFlow.collectAsState() val reactionsState by flowSet.reactions.stateFlow.collectAsState() val zapsState by flowSet.zaps.stateFlow.collectAsState() + // A reply can be a placeholder until its event arrives. + // Note.loadEvent invalidates the metadata slot, so key + // the read on it rather than reading note.event bare — + // an unread collectAsState recomposes nothing. + val event = remember(note, metadataState) { note.event } + DisposableEffect(note) { onDispose { note.clearFlow() } } + // getOrCreate, not getIfExists: the reply's author is + // often a stranger the cache has no User for yet, and a + // null here would leave the row with nothing to observe + // and nothing to subscribe to. val author = - remember(event?.pubKey, metadataState) { - event?.pubKey?.let { localCache.getUserIfExists(it) } + remember(event?.pubKey) { + event?.pubKey?.let { localCache.getOrCreateUser(it) } } + // Ask for the author's kind-0 while this row is composed. + // A reply renders through CommentItem, not NoteCard, so + // it does not inherit NoteCard's UserFinder — without + // this nothing ever requests these profiles and the row + // shows a truncated pubkey forever. + if (author != null) { + UserFinderFilterAssemblerSubscription(author) + } + // The reply almost always renders before its author's // kind-0 does, so the name and avatar have to come from // an observation rather than the lookup above. From 8f38d4a0b9761d89d39f93c41fe01d1129609265 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 02:23:40 +0000 Subject: [PATCH 11/29] docs(commons): correct what blocks the event cache from commonMain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier note said the NIP-95 `java.io.File` spill was the only thing keeping the cache out of `commonMain`. That was wrong. The binding blocker is that the cache's own storage — `LargeSoftCache`, holding `users`, `notes` and `addressables` — is itself `jvmAndroid`, as are both `*ListMatchingFilter` observables, `MintDirectoryIndex` and `NwcPaymentTracker`. `EventCache` cannot go without them. The audit also found most of it is already answered in the tree: `commons.util.WeakReference` is an expect/actual that already anticipates iOS, quartz's commonMain ships `ConcurrentMap`/`ConcurrentSet`/`LargeCache`, and `LargeSoftCache`'s skip-list ordering is not load-bearing — its surface has no ordered read, so the skip list is there for concurrency alone. That leaves two pieces of real work: an okio or `expect` blob sink, and getting `java.util.SortedSet` out of `EventCache.filter`'s signature, which has no common equivalent and ripples into `CacheSearch`, both observables and the parity test. Recorded with a bottom-up order, and noted that none of it is on part B's critical path — Desktop is JVM. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- commons/ARCHITECTURE.md | 16 +++-- .../2026-08-30-commons-migration-sweep.md | 63 +++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/commons/ARCHITECTURE.md b/commons/ARCHITECTURE.md index b28e0bee01..01aec760b2 100644 --- a/commons/ARCHITECTURE.md +++ b/commons/ARCHITECTURE.md @@ -253,10 +253,18 @@ These are intentionally *documented*, not silently tolerated. Fix opportunistica namespaces expected to grow; do not fold them into `util` just for size. - **`onchain`** (on-chain zap splitting) is `quartz`-adjacent but un-numbered; leave readable unless a clear NIP number lands. -- **`model/cache/LocalCache` is `jvmAndroid`, not `commonMain`**, because it - spills NIP-95 blobs through `java.io.File`. Promoting it needs an okio (or - `expect`) sink behind `LocalCacheHost.nip95BlobDir`; nothing else in the - move-group blocks iOS. +- **`model/cache/EventCache` is `jvmAndroid`, not `commonMain`.** The NIP-95 + `java.io.File` spill is the obvious blocker but not the binding one: the + cache's own storage, `LargeSoftCache`, is `jvmAndroid` + (`WeakReference` + `ConcurrentSkipListMap`), as are the two + `*ListMatchingFilter` observables, `MintDirectoryIndex` and + `NwcPaymentTracker`. Promoting the cache means promoting those first. + Most of it has answers already in the tree — `commons.util.WeakReference` + is an expect/actual, and quartz's `commonMain` has `ConcurrentMap`, + `ConcurrentSet` and `LargeCache` — so the genuinely new work is an okio (or + `expect`) blob sink and getting `java.util.SortedSet` out of + `EventCache.filter`'s signature, which has no common equivalent. See the + audit in `commons/plans/2026-08-30-commons-migration-sweep.md`. --- diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index 043c23fd6f..84f9780e06 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1000,3 +1000,66 @@ GC-eligible on Desktop, as they already are on Android. **The test suite is not sufficient for this step.** 62 tests cover the consume path well, but nothing covers a stale render. Run the desktop app against a real relay before calling part B done. + + +### Can `EventCache` go to `commonMain`? — audit (2026-09-21) + +Short answer: yes in principle, but it is a **port, not a move**, and the +earlier note in this file ("needs an okio sink … nothing else in the +move-group blocks iOS") was wrong. The file sink is the visible blocker; the +binding one is that the cache's own storage is `jvmAndroid`. + +`commons` really does build `iosArm64`/`iosSimulatorArm64`, so this is a real +constraint rather than a hypothetical one. + +**What must move with it** (`EventCache` cannot go alone): + +| Dependency | Why it is `jvmAndroid` | +|---|---| +| `LargeSoftCache` — `users`, `notes`, `addressables` | `java.lang.ref.WeakReference`, `ConcurrentSkipListMap`, `BiConsumer` | +| `EventListMatchingFilter`, `NoteListMatchingFilter` | `SortedSet`, `ConcurrentHashMap`, `ConcurrentSkipListSet` | +| `MintDirectoryIndex` | `ConcurrentHashMap` | +| `NwcPaymentTracker` | `ConcurrentHashMap`, `AtomicInteger` | +| `DvmHeartbeatRegistry`, `OnchainZapResolver` | `ConcurrentHashMap` | +| `LocalCacheHost` | `java.io.File` | + +**Most of it already has an answer in the tree:** + +- `commons.util.WeakReference` is already an `expect`/`actual`, and its KDoc + already anticipates `kotlin.native.ref.WeakReference` for iOS. +- quartz `commonMain` already ships `ConcurrentMap`, `ConcurrentSet`, + `ConcurrentHashCache` and `LargeCache` — so every `ConcurrentHashMap` / + `ConcurrentSkipListSet` above is a swap, not a design problem. +- `LargeSoftCache`'s skip-list ordering is **not** load-bearing: its whole + surface is `get`/`put`/`remove`/`size`/`containsKey`/`keys`/`forEach`, with + no ordered read. `ConcurrentSkipListMap` is there for lock-free concurrency, + so quartz's `ConcurrentMap` should substitute — worth confirming nothing + iterates expecting order before relying on that. +- `AtomicInteger` → `kotlin.concurrent.atomics`; `BiConsumer` → a function + type; the `dateFormatter` call is one log line and can go. +- `androidx.collection.LruCache` in `AntiSpamFilter` is already KMP — + `commonMain` uses it in `blurhash/CosineCache` and `relays/EOSE`. + +**The two pieces of genuinely new work:** + +1. **NIP-95 blob sink.** `EventCache.consume(FileStorageEvent)` writes through + `java.io.File`/`FileOutputStream`. Needs okio or an `expect` sink behind + `LocalCacheHost.nip95BlobDir`. +2. **`java.util.SortedSet` in the public API.** `EventCache.filter(Filter)` + returns one, and both `*ListMatchingFilter` observables take + `(Filter) -> SortedSet`. Kotlin has no common `SortedSet`, so this is + a signature change rippling into `CacheSearch`, both observables and + `LocalCacheSearchParityTest`. + +**Recommended order** (bottom-up; each step is independently shippable): + +1. `LargeSoftCache` → `commonMain` on the existing `WeakReference` expect plus + quartz's `ConcurrentMap`. This is the keystone — everything else is behind it. +2. The `ConcurrentHashMap`/`ConcurrentSkipListSet` holders, mechanically. +3. `SortedSet` out of `EventCache.filter`'s signature. +4. The NIP-95 sink. +5. `EventCache` + `LocalCacheHost` themselves. + +**Not on part B's critical path.** Desktop is JVM, so retiring +`DesktopLocalCache` needs none of this. The payoff here is an iOS front end +later, not anything queued now. From 5139ccef1ee33117a3561c033005e6af33aa7c2b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 02:36:59 +0000 Subject: [PATCH 12/29] docs(commons): the cache's sorted store is what blocks commonMain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correcting yesterday's audit, which said `LargeSoftCache`'s skip-list ordering was not load-bearing and that quartz's `ConcurrentMap` could substitute. Both halves were wrong, from reading only the first half of the file. `LargeSoftCache` implements `CacheOperations` — itself `quartz/jvmAndroid`, not commonMain — for its ranged `forEach(from, to, …)`, backed by `ConcurrentSkipListMap.subMap`. All of `LargeSoftCacheAddressExt` is built on it: `filter(kindStart(kind), kindEnd(kind), …)` walks one kind's slice of the `Address` key space, and `filterIntoSet` has 28 callers. On a hash map each of those degrades to a full scan of every addressable. `quartz/linuxTest/LargeCacheRangeFallbackTest` states the same invariant from the other side: the native range overloads fall back to full scans, and that is deemed safe *because* "the range overloads have no callers outside the JVM-only LargeSoftCache". Promoting the cache makes that false. So the remaining work is not the okio sink plus a `SortedSet` signature change. It needs a sorted KMP store first — an expect/actual `LargeSoftCache` with a hand-written sorted iOS actual, or a KMP sorted-concurrent map that exists in neither the tree nor the stdlib. Recorded as a question to answer before the ordered steps, not as one of them. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- commons/ARCHITECTURE.md | 16 ++++++---- .../2026-08-30-commons-migration-sweep.md | 32 +++++++++++++++---- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/commons/ARCHITECTURE.md b/commons/ARCHITECTURE.md index 01aec760b2..00aaa43c71 100644 --- a/commons/ARCHITECTURE.md +++ b/commons/ARCHITECTURE.md @@ -259,12 +259,16 @@ These are intentionally *documented*, not silently tolerated. Fix opportunistica (`WeakReference` + `ConcurrentSkipListMap`), as are the two `*ListMatchingFilter` observables, `MintDirectoryIndex` and `NwcPaymentTracker`. Promoting the cache means promoting those first. - Most of it has answers already in the tree — `commons.util.WeakReference` - is an expect/actual, and quartz's `commonMain` has `ConcurrentMap`, - `ConcurrentSet` and `LargeCache` — so the genuinely new work is an okio (or - `expect`) blob sink and getting `java.util.SortedSet` out of - `EventCache.filter`'s signature, which has no common equivalent. See the - audit in `commons/plans/2026-08-30-commons-migration-sweep.md`. + The binding constraint is `LargeSoftCache`'s **sorted** store: it backs the + ranged `forEach(from, to, …)` that all of `LargeSoftCacheAddressExt` uses to + scan one kind's slice of the `Address` key space, and a hash map turns each + of those into a full scan. `quartz/linuxTest/LargeCacheRangeFallbackTest` + documents the matching invariant from the other side — the native range + overloads fall back to full scans, and that is deemed safe precisely + *because* the range callers live in the JVM-only `LargeSoftCache`. Moving + this needs a sorted KMP store first, not just the okio blob sink and the + `java.util.SortedSet` signature change. See the audit in + `commons/plans/2026-08-30-commons-migration-sweep.md`. --- diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index 84f9780e06..9bc05d303b 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1030,11 +1030,15 @@ constraint rather than a hypothetical one. - quartz `commonMain` already ships `ConcurrentMap`, `ConcurrentSet`, `ConcurrentHashCache` and `LargeCache` — so every `ConcurrentHashMap` / `ConcurrentSkipListSet` above is a swap, not a design problem. -- `LargeSoftCache`'s skip-list ordering is **not** load-bearing: its whole - surface is `get`/`put`/`remove`/`size`/`containsKey`/`keys`/`forEach`, with - no ordered read. `ConcurrentSkipListMap` is there for lock-free concurrency, - so quartz's `ConcurrentMap` should substitute — worth confirming nothing - iterates expecting order before relying on that. +- ~~`LargeSoftCache`'s skip-list ordering is not load-bearing~~ — **wrong, + corrected 2026-09-21.** It is load-bearing, and it is the thing that stops + this migration. `LargeSoftCache` implements `CacheOperations` (which is + itself `quartz/jvmAndroid`, not commonMain) for its ranged + `forEach(from, to, …)`, backed by `ConcurrentSkipListMap.subMap`. The whole + of `LargeSoftCacheAddressExt` is built on it: `filter(kindStart(kind), + kindEnd(kind), …)` walks only one kind's slice of the `Address` key space, + and `filterIntoSet` is called 28 times. On a hash map every one of those + becomes a full scan of all addressables. - `AtomicInteger` → `kotlin.concurrent.atomics`; `BiConsumer` → a function type; the `dateFormatter` call is one log line and can go. - `androidx.collection.LruCache` in `AntiSpamFilter` is already KMP — @@ -1051,7 +1055,23 @@ constraint rather than a hypothetical one. a signature change rippling into `CacheSearch`, both observables and `LocalCacheSearchParityTest`. -**Recommended order** (bottom-up; each step is independently shippable): +**The invariant this would break.** `quartz/linuxTest/LargeCacheRangeFallbackTest` +pins the native `LargeCache` range overloads to a deliberate full-scan +fallback, and says why that is acceptable: *"the range overloads have no +callers outside the JVM-only `LargeSoftCache`"*. Promoting `LargeSoftCache` to +`commonMain` makes that statement false. Any move has to either give iOS a +genuinely sorted store or accept — explicitly, not silently — that addressable +lookups there are full scans. + +**Revised cost.** This is not the swap described above. It needs either an +`expect`/`actual` `LargeSoftCache` with a hand-written sorted iOS actual (the +`LargeCache` apple actual, for comparison, is 432 lines), or a KMP +sorted-concurrent map that does not exist in the tree or the stdlib — which +would want a `Comparable` bound on `Address` and would benefit quartz too. +Neither is a step; both are projects. + +**Recommended order** (bottom-up; each step is independently shippable), *if +the ordering question above is answered first*: 1. `LargeSoftCache` → `commonMain` on the existing `WeakReference` expect plus quartz's `ConcurrentMap`. This is the keystone — everything else is behind it. From b25e8e73673e5f727878b06b6a49b2c016f30a96 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 12:38:21 +0000 Subject: [PATCH 13/29] refactor(commons): make LargeSoftCache expect/actual, iOS unimplemented MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the cache's own storage into commonMain as an expect class so the rest of EventCache can follow. The JVM/Android actual is today's ConcurrentSkipListMap of WeakReferences, unchanged in behaviour; only size() needed an `actual override`, since the CacheOperations defaults actualize the remaining expect members through inheritance. The java.lang.ref.WeakReference became the commons WeakReference expect, which is an actual typealias to it on this target. The iOS actual throws on every member, on purpose. Kotlin/Native has neither a sorted concurrent map nor a weak-valued one, and the cache needs both: values are held weakly so nothing is retained by the cache alone, and keys are sorted so the ranged scans behind LargeSoftCacheAddressExt touch only their sub-range. A HashMap actual would compile and then be wrong at runtime in precisely the way quartz's LargeCacheRangeFallbackTest warns about — every kind lookup walking every addressable — with no test to catch it. The stub keeps :commons compiling for iOS, fails loudly rather than silently, and leaves a written-down contract for whoever writes the real one. LargeSoftCacheAddressExt moves to commonMain too: its ranged calls are all ICacheOperations members, so it needs no platform code. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../2026-08-30-commons-migration-sweep.md | 41 +++ .../commons/model/cache/LargeSoftCache.kt | 236 +++++++++++++++++ .../model/cache/LargeSoftCacheAddressExt.kt | 0 .../commons/model/cache/LargeSoftCache.ios.kt | 237 ++++++++++++++++++ ...tCache.kt => LargeSoftCache.jvmAndroid.kt} | 39 +-- 5 files changed, 538 insertions(+), 15 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCache.kt rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCacheAddressExt.kt (100%) create mode 100644 commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCache.ios.kt rename commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/{LargeSoftCache.kt => LargeSoftCache.jvmAndroid.kt} (82%) diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index 9bc05d303b..a28bca0e7a 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1083,3 +1083,44 @@ the ordering question above is answered first*: **Not on part B's critical path.** Desktop is JVM, so retiring `DesktopLocalCache` needs none of this. The payoff here is an iOS front end later, not anything queued now. + +### Step 1 shipped: `LargeSoftCache` is now `expect`/`actual` (iOS unimplemented) + +Decision on the ordering question above: **`expect`/`actual`, with the iOS +actual deliberately missing.** Not a hash-map actual, and not a full sorted +concurrent map yet — the third option, which keeps the invariant honest at the +cost of iOS not being able to construct the cache. + +- `model/cache/LargeSoftCache.kt` → `commonMain`, as + `expect class LargeSoftCache() : ICacheOperations`. + Like quartz's `LargeCache`, the expect has to redeclare every member it + inherits from the interface — an expect class is not abstract, so the + inherited abstracts are its own to declare. +- `LargeSoftCache.jvmAndroid.kt` is today's implementation unchanged in + behaviour: `ConcurrentSkipListMap>` behind + `CacheOperations`. The collector defaults and both `forEach(BiConsumer)` + overloads stay JVM-side; only `size()` needed an `actual override`, the rest + actualize through inheritance. `java.lang.ref.WeakReference` became the + commons `WeakReference` expect — the same type on JVM, it is an `actual + typealias`. +- `LargeSoftCache.ios.kt` **throws on every member.** Kotlin/Native has no + sorted concurrent map and no weak-valued one; a `HashMap` actual would + compile and then be wrong in exactly the way `LargeCacheRangeFallbackTest` + warns about — every bounded scan silently walking every entry. The stub + keeps `:commons` compiling for iOS and fails loudly if anything there tries + to construct a cache. Replacing it is now a self-contained task with a + written-down contract (key-sorted, weak values) rather than a blocker on the + rest of the migration. +- `LargeSoftCacheAddressExt.kt` moved to `commonMain` with it — its ranged + `filter`/`filterIntoSet`/`mapNotNullIntoSet` calls are all `ICacheOperations` + members, so they need no platform code. + +Verified: `:commons:compileCommonMainKotlinMetadata`, +`:commons:compileIosMainKotlinMetadata` (this runs on Linux and *does* enforce +expect/actual matching — checked by breaking a member name on purpose and +watching it fail), `:commons:verifyKmpPurity`, `:commons:jvmTest`, +`:desktopApp:test`, `:cli:test`, `:amethyst:compileFdroidDebugKotlin`, +`LargeCacheAddressableFilterTest`, `spotlessCheck`. + +Steps 2–5 are unchanged and still ahead; the iOS actual is now their peer, not +their gate. diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCache.kt new file mode 100644 index 0000000000..fd60ec9a03 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCache.kt @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.model.cache + +import com.vitorpamplona.amethyst.commons.util.WeakReference +import com.vitorpamplona.quartz.utils.cache.CacheCollectors +import com.vitorpamplona.quartz.utils.cache.ICacheBiConsumer +import com.vitorpamplona.quartz.utils.cache.ICacheOperations + +/** + * A **sorted**, thread-safe cache whose values are held by [WeakReference], so an + * entry no other part of the app references is free to be collected. + * + * Two properties are load-bearing and any `actual` must preserve both: + * + * 1. **Weak values.** The cache alone never keeps an object alive. Callers that + * need a value to survive must hold it themselves — see [getOrCreate], which + * returns the live instance and re-creates it after a collection. + * 2. **Key ordering.** Iteration follows the natural order of [K], and the + * ranged `from`/`to` operations inherited from [ICacheOperations] (the + * `filter(from, to, …)` family behind `LargeSoftCacheAddressExt.kt`) must scan + * only that sub-range. Backing this with a hash map would still *compile*, + * but would silently turn every bounded scan — the kind lookups the feeds + * run constantly — into a full scan of every entry. + * + * The JVM/Android `actual` is a `ConcurrentSkipListMap` of weak references, + * which gives both for free. iOS has neither a sorted concurrent map nor a + * weak-valued one in its standard library, so its actual is a stub that throws + * — see `LargeSoftCache.ios.kt`. + */ +expect class LargeSoftCache() : ICacheOperations { + fun keys(): Set + + fun get(key: K): V? + + fun remove(key: K) + + /** Removes [key] only while it still maps to [value]; returns true if it did. */ + fun removeIf( + key: K, + value: WeakReference, + ): Boolean + + fun isEmpty(): Boolean + + fun clear() + + fun containsKey(key: K): Boolean + + /** Puts [value] under [key], held weakly. */ + fun put( + key: K, + value: V, + ) + + /** + * Returns the live value for [key], building and caching a new one when the + * key is absent or its referent has already been collected. + */ + fun getOrCreate( + key: K, + builder: (key: K) -> V, + ): V + + /** Drops every entry whose referent has already been collected. */ + fun cleanUp() + + // Read-side operations inherited from ICacheOperations. An expect class has + // to declare the abstract members it inherits, same as quartz's LargeCache. + override fun size(): Int + + override fun forEach(consumer: ICacheBiConsumer): Unit + + override fun filter(consumer: CacheCollectors.BiFilter): List + + override fun filterIntoSet(consumer: CacheCollectors.BiFilter): Set + + override fun map(consumer: CacheCollectors.BiNotNullMapper): List + + override fun mapNotNull(consumer: CacheCollectors.BiMapper): List + + override fun mapNotNullIntoSet(consumer: CacheCollectors.BiMapper): Set + + override fun mapFlatten(consumer: CacheCollectors.BiMapper?>): List + + override fun mapFlattenIntoSet(consumer: CacheCollectors.BiMapper?>): Set + + override fun maxOrNullOf( + filter: CacheCollectors.BiFilter, + comparator: Comparator, + ): V? + + override fun sumOf(consumer: CacheCollectors.BiSumOf): Int + + override fun sumOfLong(consumer: CacheCollectors.BiSumOfLong): Long + + override fun groupBy(consumer: CacheCollectors.BiNotNullMapper): Map> + + override fun countByGroup(consumer: CacheCollectors.BiNotNullMapper): Map + + override fun sumByGroup( + groupMap: CacheCollectors.BiNotNullMapper, + sumOf: CacheCollectors.BiNotNullMapper, + ): Map + + override fun count(consumer: CacheCollectors.BiFilter): Int + + override fun associate(transform: (K, V) -> Pair): Map + + override fun associateWith(transform: (K, V) -> U?): Map + + override fun filter( + from: K, + to: K, + consumer: CacheCollectors.BiFilter, + ): List + + override fun filterIntoSet( + from: K, + to: K, + consumer: CacheCollectors.BiFilter, + ): Set + + override fun map( + from: K, + to: K, + consumer: CacheCollectors.BiNotNullMapper, + ): List + + override fun mapNotNull( + from: K, + to: K, + consumer: CacheCollectors.BiMapper, + ): List + + override fun mapNotNullIntoSet( + from: K, + to: K, + consumer: CacheCollectors.BiMapper, + ): Set + + override fun mapFlatten( + from: K, + to: K, + consumer: CacheCollectors.BiMapper?>, + ): List + + override fun mapFlattenIntoSet( + from: K, + to: K, + consumer: CacheCollectors.BiMapper?>, + ): Set + + override fun maxOrNullOf( + from: K, + to: K, + filter: CacheCollectors.BiFilter, + comparator: Comparator, + ): V? + + override fun sumOf( + from: K, + to: K, + consumer: CacheCollectors.BiSumOf, + ): Int + + override fun sumOfLong( + from: K, + to: K, + consumer: CacheCollectors.BiSumOfLong, + ): Long + + override fun groupBy( + from: K, + to: K, + consumer: CacheCollectors.BiNotNullMapper, + ): Map> + + override fun countByGroup( + from: K, + to: K, + consumer: CacheCollectors.BiNotNullMapper, + ): Map + + override fun sumByGroup( + from: K, + to: K, + groupMap: CacheCollectors.BiNotNullMapper, + sumOf: CacheCollectors.BiNotNullMapper, + ): Map + + override fun count( + from: K, + to: K, + consumer: CacheCollectors.BiFilter, + ): Int + + override fun associate( + from: K, + to: K, + transform: (K, V) -> Pair, + ): Map + + override fun associateWith( + from: K, + to: K, + transform: (K, V) -> U?, + ): Map + + override fun joinToString( + separator: CharSequence, + prefix: CharSequence, + postfix: CharSequence, + limit: Int, + truncated: CharSequence, + transform: ((K, V) -> CharSequence)?, + ): String +} diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCacheAddressExt.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCacheAddressExt.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCacheAddressExt.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCacheAddressExt.kt diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCache.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCache.ios.kt new file mode 100644 index 0000000000..679c955c54 --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCache.ios.kt @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.model.cache + +import com.vitorpamplona.amethyst.commons.util.WeakReference +import com.vitorpamplona.quartz.utils.cache.CacheCollectors +import com.vitorpamplona.quartz.utils.cache.ICacheBiConsumer +import com.vitorpamplona.quartz.utils.cache.ICacheOperations + +/** + * **iOS has no implementation of this cache yet.** Every member throws. + * + * The expect declaration needs a store that is sorted by key *and* holds its + * values weakly, and Kotlin/Native ships neither: `kotlin.collections` has no + * concurrent sorted map, and `kotlin.native.ref.WeakReference` (what + * [WeakReference] maps to here) covers only the reference side. Writing a + * plain `HashMap` actual would compile and then be wrong at runtime in a way + * no test would catch — the ranged scans behind `LargeSoftCacheAddressExt.kt` + * would silently walk every entry, and nothing would ever be evicted. + * + * This stub exists so `:commons` keeps compiling for iOS while the cache moves + * into commonMain. Nothing on iOS may construct it until a real actual lands: + * a sorted concurrent map (a skip list or a lock-striped sorted structure) + * whose values are weak references. + */ +actual class LargeSoftCache : ICacheOperations { + actual fun keys(): Set = notYetImplemented() + + actual fun get(key: K): V? = notYetImplemented() + + actual fun remove(key: K) { + notYetImplemented() + } + + actual fun removeIf( + key: K, + value: WeakReference, + ): Boolean = notYetImplemented() + + actual fun isEmpty(): Boolean = notYetImplemented() + + actual fun clear() { + notYetImplemented() + } + + actual fun containsKey(key: K): Boolean = notYetImplemented() + + actual fun put( + key: K, + value: V, + ) { + notYetImplemented() + } + + actual fun getOrCreate( + key: K, + builder: (key: K) -> V, + ): V = notYetImplemented() + + actual fun cleanUp() { + notYetImplemented() + } + + actual override fun size(): Int = notYetImplemented() + + actual override fun forEach(consumer: ICacheBiConsumer) { + notYetImplemented() + } + + actual override fun filter(consumer: CacheCollectors.BiFilter): List = notYetImplemented() + + actual override fun filterIntoSet(consumer: CacheCollectors.BiFilter): Set = notYetImplemented() + + actual override fun map(consumer: CacheCollectors.BiNotNullMapper): List = notYetImplemented() + + actual override fun mapNotNull(consumer: CacheCollectors.BiMapper): List = notYetImplemented() + + actual override fun mapNotNullIntoSet(consumer: CacheCollectors.BiMapper): Set = notYetImplemented() + + actual override fun mapFlatten(consumer: CacheCollectors.BiMapper?>): List = notYetImplemented() + + actual override fun mapFlattenIntoSet(consumer: CacheCollectors.BiMapper?>): Set = notYetImplemented() + + actual override fun maxOrNullOf( + filter: CacheCollectors.BiFilter, + comparator: Comparator, + ): V? = notYetImplemented() + + actual override fun sumOf(consumer: CacheCollectors.BiSumOf): Int = notYetImplemented() + + actual override fun sumOfLong(consumer: CacheCollectors.BiSumOfLong): Long = notYetImplemented() + + actual override fun groupBy(consumer: CacheCollectors.BiNotNullMapper): Map> = notYetImplemented() + + actual override fun countByGroup(consumer: CacheCollectors.BiNotNullMapper): Map = notYetImplemented() + + actual override fun sumByGroup( + groupMap: CacheCollectors.BiNotNullMapper, + sumOf: CacheCollectors.BiNotNullMapper, + ): Map = notYetImplemented() + + actual override fun count(consumer: CacheCollectors.BiFilter): Int = notYetImplemented() + + actual override fun associate(transform: (K, V) -> Pair): Map = notYetImplemented() + + actual override fun associateWith(transform: (K, V) -> U?): Map = notYetImplemented() + + actual override fun filter( + from: K, + to: K, + consumer: CacheCollectors.BiFilter, + ): List = notYetImplemented() + + actual override fun filterIntoSet( + from: K, + to: K, + consumer: CacheCollectors.BiFilter, + ): Set = notYetImplemented() + + actual override fun map( + from: K, + to: K, + consumer: CacheCollectors.BiNotNullMapper, + ): List = notYetImplemented() + + actual override fun mapNotNull( + from: K, + to: K, + consumer: CacheCollectors.BiMapper, + ): List = notYetImplemented() + + actual override fun mapNotNullIntoSet( + from: K, + to: K, + consumer: CacheCollectors.BiMapper, + ): Set = notYetImplemented() + + actual override fun mapFlatten( + from: K, + to: K, + consumer: CacheCollectors.BiMapper?>, + ): List = notYetImplemented() + + actual override fun mapFlattenIntoSet( + from: K, + to: K, + consumer: CacheCollectors.BiMapper?>, + ): Set = notYetImplemented() + + actual override fun maxOrNullOf( + from: K, + to: K, + filter: CacheCollectors.BiFilter, + comparator: Comparator, + ): V? = notYetImplemented() + + actual override fun sumOf( + from: K, + to: K, + consumer: CacheCollectors.BiSumOf, + ): Int = notYetImplemented() + + actual override fun sumOfLong( + from: K, + to: K, + consumer: CacheCollectors.BiSumOfLong, + ): Long = notYetImplemented() + + actual override fun groupBy( + from: K, + to: K, + consumer: CacheCollectors.BiNotNullMapper, + ): Map> = notYetImplemented() + + actual override fun countByGroup( + from: K, + to: K, + consumer: CacheCollectors.BiNotNullMapper, + ): Map = notYetImplemented() + + actual override fun sumByGroup( + from: K, + to: K, + groupMap: CacheCollectors.BiNotNullMapper, + sumOf: CacheCollectors.BiNotNullMapper, + ): Map = notYetImplemented() + + actual override fun count( + from: K, + to: K, + consumer: CacheCollectors.BiFilter, + ): Int = notYetImplemented() + + actual override fun associate( + from: K, + to: K, + transform: (K, V) -> Pair, + ): Map = notYetImplemented() + + actual override fun associateWith( + from: K, + to: K, + transform: (K, V) -> U?, + ): Map = notYetImplemented() + + actual override fun joinToString( + separator: CharSequence, + prefix: CharSequence, + postfix: CharSequence, + limit: Int, + truncated: CharSequence, + transform: ((K, V) -> CharSequence)?, + ): String = notYetImplemented() +} + +private fun notYetImplemented(): Nothing = + throw NotImplementedError( + "LargeSoftCache has no iOS actual yet: it needs a key-sorted concurrent map with weak values.", + ) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCache.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCache.jvmAndroid.kt similarity index 82% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCache.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCache.jvmAndroid.kt index 71bd3ba9c5..1c11cff060 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCache.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LargeSoftCache.jvmAndroid.kt @@ -20,17 +20,22 @@ */ package com.vitorpamplona.amethyst.commons.model.cache +import com.vitorpamplona.amethyst.commons.util.WeakReference import com.vitorpamplona.quartz.utils.cache.CacheOperations -import java.lang.ref.WeakReference import java.util.concurrent.ConcurrentSkipListMap import java.util.function.BiConsumer -class LargeSoftCache : CacheOperations { +/** + * A `ConcurrentSkipListMap` gives the two properties the expect declares in one + * structure: keys stay in natural order (so `subMap` serves the ranged scans) + * and reads are lock-free. + */ +actual class LargeSoftCache : CacheOperations { private val cache = ConcurrentSkipListMap>() - fun keys() = cache.keys + actual fun keys(): Set = cache.keys - fun get(key: K): V? { + actual fun get(key: K): V? { val softRef = cache.get(key) ?: return null val value = softRef.get() @@ -42,20 +47,24 @@ class LargeSoftCache : CacheOperations { } } - fun remove(key: K) = cache.remove(key) + actual fun remove(key: K) { + cache.remove(key) + } - fun removeIf( + actual fun removeIf( key: K, value: WeakReference, - ) = cache.remove(key, value) + ): Boolean = cache.remove(key, value) - override fun size() = cache.size + actual override fun size() = cache.size - fun isEmpty() = cache.isEmpty() + actual fun isEmpty(): Boolean = cache.isEmpty() - fun clear() = cache.clear() + actual fun clear() { + cache.clear() + } - fun containsKey(key: K) = cache.containsKey(key) + actual fun containsKey(key: K): Boolean = cache.containsKey(key) /** * Puts an object into the cache with a specified key. @@ -64,7 +73,7 @@ class LargeSoftCache : CacheOperations { * @param key The key to associate with the object. * @param value The object to cache. */ - fun put( + actual fun put( key: K, value: V, ) { @@ -80,7 +89,7 @@ class LargeSoftCache : CacheOperations { * @param key The key of the object to retrieve. * @return The cached object, or null if it's no longer available. */ - fun getOrCreate( + actual fun getOrCreate( key: K, builder: (key: K) -> V, ): V { @@ -107,7 +116,7 @@ class LargeSoftCache : CacheOperations { * Proactively cleans up the cache by removing entries whose weakly referenced * objects have been garbage collected. Single-pass iterator for efficiency. */ - fun cleanUp() { + actual fun cleanUp() { val iter = cache.entries.iterator() while (iter.hasNext()) { val entry = iter.next() @@ -131,7 +140,7 @@ class LargeSoftCache : CacheOperations { .forEach(BiConsumerWrapper(this, consumer)) } - class BiConsumerWrapper( + private class BiConsumerWrapper( val cache: LargeSoftCache, val inner: BiConsumer, ) : BiConsumer> { From 1ead0065b282c4b78981cae13fd402ff35561991 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 14:11:36 +0000 Subject: [PATCH 14/29] refactor(commons): move the ConcurrentHashMap holders to commonMain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MintDirectoryIndex, DvmHeartbeatRegistry and NwcPaymentTracker had nothing platform-specific left but their maps, so each swaps onto a KMP primitive that already existed in quartz. ConcurrentMap.merge and getOrPut are atomic on every target, which is what MintDirectoryIndex's count and DvmHeartbeatRegistry's one-flow-per-address actually relied on from ConcurrentHashMap; suggest() now ranks over a snapshot() the comparator cannot see shift underneath it. NwcPaymentTracker's spoofAttempts becomes kotlin.concurrent.atomics.AtomicInt, as quartz already uses in BleChunkAssembler and BanStore, and its containsKey becomes a null check on get — the same question for a map that never stores nulls. Its test moves to commonTest, so those 4 race cases now run on iOS too. OnchainZapResolver still reads EventCache and cannot move yet, but its two in-flight gates swap ConcurrentHashMap.newKeySet() for the commons ConcurrentSet, which is exactly that on JVM/Android. It now has no java.* import left, so it travels with EventCache later at no cost. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../2026-08-30-commons-migration-sweep.md | 37 +++++++++++++++++++ .../commons/cashu/MintDirectoryIndex.kt | 17 ++++++--- .../model/nip90DVMs/DvmHeartbeatRegistry.kt | 4 +- .../commons/service/nwc/NwcPaymentTracker.kt | 19 ++++++---- .../service/nwc/NwcPaymentTrackerTest.kt | 0 .../nipBCOnchainZaps/OnchainZapResolver.kt | 8 ++-- 6 files changed, 65 insertions(+), 20 deletions(-) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/cashu/MintDirectoryIndex.kt (87%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/model/nip90DVMs/DvmHeartbeatRegistry.kt (95%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTracker.kt (91%) rename commons/src/{jvmTest => commonTest}/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTrackerTest.kt (100%) diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index a28bca0e7a..5619e09297 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1124,3 +1124,40 @@ watching it fail), `:commons:verifyKmpPurity`, `:commons:jvmTest`, Steps 2–5 are unchanged and still ahead; the iOS actual is now their peer, not their gate. + +### Step 2 shipped: the `ConcurrentHashMap` holders + +Three of the six holders in the audit table above are now `commonMain`, each a +straight swap onto a KMP primitive that already existed: + +- `MintDirectoryIndex` — `ConcurrentHashMap` → quartz + `ConcurrentMap`. `counts.merge(key, 1, Int::plus)` becomes + `merge(key, 1) { old, new -> old + new }`, which is atomic on every target, + so a count still cannot be lost to a racing `add`. `suggest()` now ranks over + `snapshot()` — a copy the comparator cannot see shift underneath it. +- `DvmHeartbeatRegistry` — `ConcurrentMap.getOrPut` is the same atomic + get-or-create the `ConcurrentHashMap` version relied on, so two threads + racing the first beat for an address still agree on one `MutableStateFlow`. +- `NwcPaymentTracker` — `ConcurrentMap` plus `kotlin.concurrent.atomics.AtomicInt` + for `spoofAttempts` (`incrementAndGet()` → `fetchAndIncrement()`, `get()` → + `load()`, under `@OptIn(ExperimentalAtomicApi::class)` as quartz already does + in `BleChunkAssembler` and `BanStore`). `containsKey` has no `ConcurrentMap` + equivalent and became a null check on `get`, which is the same question for a + map that never stores nulls. Its test moved to `commonTest`, so the tracker's + 4 race cases now also run on iOS rather than JVM only. + +`OnchainZapResolver` cannot move yet — it reads `EventCache` — but its two +`ConcurrentHashMap.newKeySet()` in-flight gates are now the commons +`ConcurrentSet`, which is exactly `newKeySet()` on JVM/Android. That leaves it +with no `java.*` import, so it travels with `EventCache` in step 5 for free. + +`MintDirectoryIndexTest` stayed in `jvmTest`: it is written against +`org.junit.Assert`, and converting it to `kotlin.test` is unrelated churn. + +Remaining in the table: `EventListMatchingFilter` / `NoteListMatchingFilter`, +which also carry `SortedSet` — they belong to step 3, not this one. + +Verified: `:commons:compileCommonMainKotlinMetadata`, +`:commons:compileIosMainKotlinMetadata`, `:commons:jvmTest` (incl. the moved +`NwcPaymentTrackerTest`, 4 tests), `:desktopApp:test`, `:cli:test`, +`:amethyst:compileFdroidDebugKotlin`, `DvmHeartbeatTest`, `spotlessCheck`. diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/MintDirectoryIndex.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cashu/MintDirectoryIndex.kt similarity index 87% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/MintDirectoryIndex.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cashu/MintDirectoryIndex.kt index 6d61a538b0..dd6b872d6e 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/MintDirectoryIndex.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cashu/MintDirectoryIndex.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.cashu -import java.util.concurrent.ConcurrentHashMap +import com.vitorpamplona.quartz.utils.concurrent.ConcurrentMap /** * In-memory directory of Cashu mint URLs the app has seen on the network, @@ -43,14 +43,15 @@ import java.util.concurrent.ConcurrentHashMap * `https://` scheme are dropped — autocomplete on garbage tags would * surface noise. * - * Thread-safe via `ConcurrentHashMap`; safe to call from any dispatcher. + * Thread-safe via [ConcurrentMap], whose `merge` is atomic on every target, so a + * count cannot be lost to a racing [add]; safe to call from any dispatcher. */ class MintDirectoryIndex { - private val counts = ConcurrentHashMap() + private val counts = ConcurrentMap() fun add(rawUrl: String) { val key = normalize(rawUrl) ?: return - counts.merge(key, 1, Int::plus) + counts.merge(key, 1) { old, new -> old + new } } fun addAll(rawUrls: Iterable) = rawUrls.forEach(::add) @@ -69,7 +70,11 @@ class MintDirectoryIndex { val ranking = compareByDescending> { it.value } .thenBy { it.key } - return counts.entries + // snapshot() instead of a live view: the ranking sorts the whole entry set, + // and a copy cannot shift under the comparator while it does. + return counts + .snapshot() + .entries .asSequence() .filter { needle.isEmpty() || it.key.contains(needle) } .sortedWith(ranking) @@ -79,7 +84,7 @@ class MintDirectoryIndex { } /** Total number of unique mint URLs in the index. */ - fun size(): Int = counts.size + fun size(): Int = counts.size() /** Clears the index — for tests; production should let it accumulate. */ fun clear() = counts.clear() diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip90DVMs/DvmHeartbeatRegistry.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip90DVMs/DvmHeartbeatRegistry.kt similarity index 95% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip90DVMs/DvmHeartbeatRegistry.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip90DVMs/DvmHeartbeatRegistry.kt index ab7f5e4d45..2e8e9f30b2 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip90DVMs/DvmHeartbeatRegistry.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip90DVMs/DvmHeartbeatRegistry.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.amethyst.commons.model.nip90DVMs import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.utils.concurrent.ConcurrentMap import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import java.util.concurrent.ConcurrentHashMap /** * Strong, process-wide record of the latest heartbeat per DVM announcement address @@ -40,7 +40,7 @@ import java.util.concurrent.ConcurrentHashMap * One entry per DVM address ever seen; timestamps only, so it stays tiny. `0` means "no beat". */ object DvmHeartbeatRegistry { - private val latestBeatCreatedAt = ConcurrentHashMap>() + private val latestBeatCreatedAt = ConcurrentMap>() private fun flowFor(address: Address): MutableStateFlow = latestBeatCreatedAt.getOrPut(address) { MutableStateFlow(0L) } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTracker.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTracker.kt similarity index 91% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTracker.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTracker.kt index c4ec34a70d..94ba2218d7 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTracker.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTracker.kt @@ -23,8 +23,10 @@ package com.vitorpamplona.amethyst.commons.service.nwc import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicInteger +import com.vitorpamplona.quartz.utils.concurrent.ConcurrentMap +import kotlin.concurrent.atomics.AtomicInt +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.concurrent.atomics.fetchAndIncrement /** * Tracks pending NIP-47 (Nostr Wallet Connect) payment requests awaiting responses. @@ -45,6 +47,7 @@ import java.util.concurrent.atomic.AtomicInteger * the request on the relay could otherwise forge a response with their own * keypair and trick the client into displaying attacker-controlled data. */ +@OptIn(ExperimentalAtomicApi::class) class NwcPaymentTracker { /** * Data for a pending payment request. @@ -63,10 +66,10 @@ class NwcPaymentTracker { // right `e` tag but the wrong author. Surfaced to the UI when a // request times out so an actual attack can be distinguished from // an unresponsive wallet. - val spoofAttempts = AtomicInteger(0) + val spoofAttempts = AtomicInt(0) } - private val awaitingRequests = ConcurrentHashMap(10) + private val awaitingRequests = ConcurrentMap() /** * Registers a pending payment request. @@ -118,7 +121,7 @@ class NwcPaymentTracker { if (requestId == null) return MatchResult.NoMatch val pending = awaitingRequests[requestId] ?: return MatchResult.NoMatch if (pending.expectedServicePubkey != responseAuthor) { - pending.spoofAttempts.incrementAndGet() + pending.spoofAttempts.fetchAndIncrement() return MatchResult.WrongAuthor(pending.expectedServicePubkey, responseAuthor) } // Author matches — atomically remove and return. @@ -132,13 +135,13 @@ class NwcPaymentTracker { */ fun spoofAttemptsFor(requestId: HexKey?): Int { if (requestId == null) return 0 - return awaitingRequests[requestId]?.spoofAttempts?.get() ?: 0 + return awaitingRequests[requestId]?.spoofAttempts?.load() ?: 0 } /** * Checks if there's a pending request for the given ID. */ - fun hasPendingRequest(requestId: HexKey): Boolean = awaitingRequests.containsKey(requestId) + fun hasPendingRequest(requestId: HexKey): Boolean = awaitingRequests[requestId] != null /** * Manually removes a pending request (e.g., on timeout). @@ -153,5 +156,5 @@ class NwcPaymentTracker { /** * Returns count of pending requests (for debugging/monitoring). */ - fun pendingCount(): Int = awaitingRequests.size + fun pendingCount(): Int = awaitingRequests.size() } diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTrackerTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTrackerTest.kt similarity index 100% rename from commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTrackerTest.kt rename to commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTrackerTest.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt index 48c3d9b684..c2176518f3 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.commons.model.nipBCOnchainZaps import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.OnchainZapStatus import com.vitorpamplona.amethyst.commons.model.cache.EventCache +import com.vitorpamplona.amethyst.commons.util.ConcurrentSet import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nipBCOnchainZaps.verify.OnchainZapVerifier import com.vitorpamplona.quartz.nipBCOnchainZaps.verify.VerifiedOnchainZap @@ -40,7 +41,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit -import java.util.concurrent.ConcurrentHashMap /** * Coordinates NIP-BC on-chain zap verification on top of the chain backend. @@ -71,10 +71,10 @@ class OnchainZapResolver( /** * In-flight set of event ids currently being verified. Prevents two `consume()` * calls (or a `consume` plus a reverify) from issuing parallel Esplora fetches - * for the same event. `ConcurrentHashMap.newKeySet` gives lock-free atomic `add` + * for the same event. [ConcurrentSet] gives lock-free atomic `add` * returning `true` only for the inserting caller. */ - private val verifyingEventIds: MutableSet = ConcurrentHashMap.newKeySet() + private val verifyingEventIds = ConcurrentSet() /** * In-flight set of note id strings currently being reverified. Lets the on-chain @@ -83,7 +83,7 @@ class OnchainZapResolver( * backstop; this one short-circuits earlier and avoids creating async coroutines * that would just no-op. */ - private val reverifyingNoteIds: MutableSet = ConcurrentHashMap.newKeySet() + private val reverifyingNoteIds = ConcurrentSet() /** * Shared poller for the current bitcoin chain tip height. Each gallery that From 00e06cfed46a57324498e8134b1d6584b05e3fc7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 15:06:24 +0000 Subject: [PATCH 15/29] fix(commons): chunk the user-metadata REQ, unstick the quoted-note embed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things the review of this branch turned up. filterUserMetadataForKey put every author for a relay in one filter. Nothing capped it, and the thread fix earlier in this branch made it easy to hit: a thread renders every reply row in one pass, not lazily, so every reply author subscribes at once. A relay that clamps `limit` answers only part of an oversized batch and says nothing, which would have left exactly the pubkey stubs that fix set out to remove. Chunked at MAX_VALUES_PER_FILTER through the same forEachChunk that FilterMissingEvents and FeedMetadataCoordinator use. Each chunk of the update filter keeps the group's earliest EOSE, so splitting the authors never narrows the window any of them is asked about. QuotedNoteEmbed collected the note's metadata flow and never read the value, so the collection subscribed but nothing in the composition depended on it: when the fetched event landed the embed stayed on "Loading quoted note...". Same one-line shape as the thread reply rows — read note.event keyed on the metadata slot. Pre-existing, but the same bug family this branch is fixing. LocalCacheHost's KDoc pointed at LocalCache.host, which is not the property (EventCache.appHost is), and said nothing about the hazard this move creates: :nappletHost can now see LocalCache, where before it could not compile a reference. A stray one no longer fails fast on an unset Amethyst.instance; it quietly builds a second empty cache in the :napplet process. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../user/watchers/FilterUserMetadataForKey.kt | 71 +++++++++++-------- .../commons/model/cache/LocalCacheHost.kt | 10 ++- .../amethyst/desktop/ui/note/NoteCard.kt | 5 +- 3 files changed, 54 insertions(+), 32 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt index 23227231ad..db874ab832 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.commons.relayClient.user.watchers import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.forEachChunk import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.EOSEAccountFast @@ -90,27 +91,33 @@ fun filterUserMetadataForKey( } } - return perRelayUsers - .map { (relay, users) -> - val firstTimers = mutableSetOf() - val updates = mutableSetOf() + val filters = mutableListOf() - var minimumTime: Long = Long.MAX_VALUE + perRelayUsers.forEach { (relay, users) -> + val firstTimers = mutableSetOf() + val updates = mutableSetOf() - users.forEach { user -> - val time = since.since(user)?.get(relay)?.time - if (time == null || user.metadataOrNull()?.flow?.value == null) { - firstTimers.add(user.pubkeyHex) - } else { - updates.add(user.pubkeyHex) - if (time < minimumTime) { - minimumTime = time - } + var minimumTime: Long = Long.MAX_VALUE + + users.forEach { user -> + val time = since.since(user)?.get(relay)?.time + if (time == null || user.metadataOrNull()?.flow?.value == null) { + firstTimers.add(user.pubkeyHex) + } else { + updates.add(user.pubkeyHex) + if (time < minimumTime) { + minimumTime = time } } + } - listOfNotNull( - if (firstTimers.isNotEmpty()) { + // Chunked for the same reason as FilterMissingEvents and FeedMetadataCoordinator: + // a relay that clamps `limit` answers only part of an oversized batch, silently. + // One screen can ask for far more than a hundred authors at once — a thread renders + // every reply row in one pass, and each row subscribes its own author. + if (firstTimers.isNotEmpty()) { + forEachChunk(firstTimers.sorted()) { chunk -> + filters.add( RelayBasedFilter( relay = relay, filter = @@ -118,13 +125,18 @@ fun filterUserMetadataForKey( purpose = SubPurpose.PROFILE_METADATA, accountPubKeys = listOfNotNull(accountPubKey), kinds = UserMetadataForKeyKinds, - authors = firstTimers.sorted(), + authors = chunk, ), - ) - } else { - null - }, - if (updates.isNotEmpty()) { + ), + ) + } + } + + if (updates.isNotEmpty()) { + // Every chunk keeps the group's earliest EOSE: splitting the authors must not + // narrow the window any of them is asked about. + forEachChunk(updates.sorted()) { chunk -> + filters.add( RelayBasedFilter( relay = relay, filter = @@ -132,13 +144,14 @@ fun filterUserMetadataForKey( purpose = SubPurpose.PROFILE_METADATA, accountPubKeys = listOfNotNull(accountPubKey), kinds = UserMetadataForKeyKinds, - authors = updates.sorted(), + authors = chunk, since = minimumTime, ), - ) - } else { - null - }, - ) - }.flatten() + ), + ) + } + } + } + + return filters } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt index ba9d6814e4..27cf2884f1 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt @@ -34,11 +34,17 @@ import java.io.File * The cache itself is a plain in-memory store and can run without any of them, so every * member here has a default that is the honest answer for a host that does not provide it * — an own scope, no NIP-95 spill directory, no relay identity, no stats sink. A front end - * installs its own via [LocalCache.host]; Android's `Amethyst` does so at startup, Desktop - * and unit tests keep the defaults. + * installs its own via [EventCache.appHost]; Android's `Amethyst` does so at startup, + * Desktop and unit tests keep the defaults. * * This exists so [LocalCache] can live in `commons` without naming `Amethyst.instance`, * `BuildConfig` or `android.os.Looper`. + * + * **Android's `:napplet` process must never reach the cache.** Now that [LocalCache] lives + * in `commons`, `:nappletHost` can see it, where before it could not compile a reference at + * all. A stray one there would no longer fail fast on an unset `Amethyst.instance` — it + * would quietly build a second, empty cache in that process, with its own IO scope nothing + * cancels. Processes share no memory: the populated cache exists only in `main`. */ interface LocalCacheHost { /** Scope for the cache's own background work: NIP-BC chain lookups and LNURL fetches. */ diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt index 0ab4912125..770479b8a8 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt @@ -528,7 +528,10 @@ fun QuotedNoteEmbed( onDispose { note.clearFlow() } } - val event = note.event + // Keyed on metadataState, not read bare: Note.loadEvent invalidates the metadata + // slot when the fetch lands, and an unread collectAsState recomposes nothing — the + // embed would sit on "Loading quoted note..." forever. + val event = remember(note, metadataState) { note.event } if (event != null) { // Re-derives when the author's kind-0 lands, so the embed's name and avatar fill in // after the note itself. From 624ac88eee70c3f83fac75cc3c0b54fb56ad9d21 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 15:22:19 +0000 Subject: [PATCH 16/29] refactor(commons): spill NIP-95 blobs through a KMP store, not java.io.File MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LocalCacheHost's nip95BlobDir was the last java.* type in the port, and a directory is not what the cache needs — it needs somewhere to put bytes and a way to ask whether they are already there. That is now Nip95BlobStore, with an okio-backed FileSystemNip95BlobStore in commonMain; okio was already a commons dependency for DeferredDeleteFileSystem, so nothing new is linked in. Its constructor takes the platform's own path string, so the Android host passes modules.nip95cache.absolutePath and needs no okio of its own. okio declares FileSystem.SYSTEM per platform rather than in common, hence the small platformFileSystem expect/actual beside it. Two things the rewrite had to face that the old block did not: decode() returns ByteArray?, which the old code passed straight into FileOutputStream.write — a platform type, so the compiler allowed it and a null would have thrown NPE past the IOException catch. It is now checked, and undecodable content simply is not spilled. The note's copy dropped its content whenever a blob directory existed, even when the write had just failed with an IOException — losing the only copy of those bytes. Content is now dropped only when the blob really is in the store, whether this call wrote it or found it already there. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../amethyst/AmethystLocalCacheHost.kt | 7 +- .../commons/model/cache/Nip95BlobStore.kt | 75 +++++++++++++++++++ .../amethyst/commons/util/SystemFileSystem.kt | 31 ++++++++ .../commons/util/SystemFileSystem.ios.kt | 25 +++++++ .../commons/model/cache/EventCache.kt | 52 +++++++------ .../commons/model/cache/LocalCacheHost.kt | 11 ++- .../util/SystemFileSystem.jvmAndroid.kt | 25 +++++++ 7 files changed, 191 insertions(+), 35 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/Nip95BlobStore.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/SystemFileSystem.kt create mode 100644 commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/SystemFileSystem.ios.kt create mode 100644 commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/SystemFileSystem.jvmAndroid.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt index 0d7fd603b1..b935b7a401 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt @@ -20,14 +20,15 @@ */ package com.vitorpamplona.amethyst +import com.vitorpamplona.amethyst.commons.model.cache.FileSystemNip95BlobStore import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.LocalCacheHost +import com.vitorpamplona.amethyst.commons.model.cache.Nip95BlobStore import com.vitorpamplona.amethyst.service.checkNotInMainThread 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 kotlinx.coroutines.CoroutineScope -import java.io.File /** * Binds the shared [LocalCache] in `commons` to this app's shell. @@ -45,7 +46,9 @@ class AmethystLocalCacheHost( ) : LocalCacheHost { override val scope: CoroutineScope get() = modules.applicationIOScope - override val nip95BlobDir: File get() = modules.nip95cache + // by lazy, not a getter: modules.nip95cache is itself lazy and creating the directory + // is the point of touching it, so this must not happen once per NIP-95 event. + override val nip95Blobs: Nip95BlobStore by lazy { FileSystemNip95BlobStore(modules.nip95cache.absolutePath) } override val relayStats: RelayStats get() = modules.relayStats diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/Nip95BlobStore.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/Nip95BlobStore.kt new file mode 100644 index 0000000000..e35c28513f --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/Nip95BlobStore.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.commons.model.cache + +import com.vitorpamplona.amethyst.commons.util.platformFileSystem +import com.vitorpamplona.quartz.utils.Log +import okio.FileSystem +import okio.IOException +import okio.Path +import okio.Path.Companion.toPath + +/** + * Where a NIP-95 blob's bytes go so they can leave memory. + * + * The cache does not need a directory, it needs somewhere to put bytes and a way to ask whether + * they are already there — which is all this is. A front end that has nowhere to spill to simply + * supplies none, and the event keeps its content in memory. + */ +interface Nip95BlobStore { + /** Whether a blob is already stored under [id]. */ + fun exists(id: String): Boolean + + /** Stores [bytes] under [id]. Returns false when the write failed. */ + fun store( + id: String, + bytes: ByteArray, + ): Boolean +} + +/** + * The ordinary [Nip95BlobStore]: one file per event id under [directory]. + * + * okio rather than `java.io.File` because this sits behind `LocalCacheHost`, which the event + * cache reads on every NIP-95 event, and the cache is shared code. + */ +class FileSystemNip95BlobStore( + private val directory: Path, + private val fileSystem: FileSystem = platformFileSystem, +) : Nip95BlobStore { + /** Takes the platform's own path string, so callers need no okio of their own. */ + constructor(directoryPath: String) : this(directoryPath.toPath()) + + override fun exists(id: String): Boolean = fileSystem.exists(directory / id) + + override fun store( + id: String, + bytes: ByteArray, + ): Boolean = + try { + fileSystem.createDirectories(directory) + fileSystem.write(directory / id) { write(bytes) } + true + } catch (e: IOException) { + Log.e("Nip95BlobStore", "Could not store the NIP-95 blob $id", e) + false + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/SystemFileSystem.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/SystemFileSystem.kt new file mode 100644 index 0000000000..e45d6e58ee --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/SystemFileSystem.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.commons.util + +import okio.FileSystem + +/** + * The real filesystem for this target. + * + * okio declares `FileSystem.SYSTEM` per platform rather than in common — there is no such thing + * on JS — so shared code that wants the ordinary filesystem has to come through here. + */ +expect val platformFileSystem: FileSystem diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/SystemFileSystem.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/SystemFileSystem.ios.kt new file mode 100644 index 0000000000..2ca484b1cd --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/SystemFileSystem.ios.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.amethyst.commons.util + +import okio.FileSystem + +actual val platformFileSystem: FileSystem = FileSystem.SYSTEM diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt index b1a59820f6..4edd119a23 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt @@ -422,9 +422,6 @@ import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import java.io.File -import java.io.FileOutputStream -import java.io.IOException import java.util.SortedSet /** @@ -3084,42 +3081,43 @@ open class EventCache : note.addRelay(relay) } - // A host with no blob directory keeps the bytes in memory below instead of spilling them. - val cachePath = appHost.nip95BlobDir + // A host with nowhere to spill keeps the bytes in memory below instead. + val blobs = appHost.nip95Blobs + + // Whether the bytes are in the store now. The note's copy may only drop its content + // once this is true — a store that refused the write is not somewhere to drop it. + var spilled = blobs != null && blobs.exists(event.id) val isVerified = - if (cachePath == null) { + if (blobs == null || spilled) { wasVerified - } else { - try { - cachePath.mkdirs() - val file = File(cachePath, event.id) - if (!file.exists() && (wasVerified || justVerify(event))) { - FileOutputStream(file).use { stream -> - stream.write(event.decode()) - } - Log.i( - "FileStorageEvent", - "NIP95 File received from $relay and saved to disk as $file", - ) - true - } else { - wasVerified - } - } catch (e: IOException) { - Log.e("FileStorageEvent", "FileStorageEvent save to disk error: " + event.id, e) + } else if (wasVerified || justVerify(event)) { + // decode() is null when the content is not the base64 the event claims, so + // there is nothing to spill and nothing this call can vouch for. + val bytes = event.decode() + if (bytes != null && blobs.store(event.id, bytes)) { + spilled = true + Log.i( + "FileStorageEvent", + "NIP95 File received from $relay and stored as ${event.id}", + ) + true + } else { wasVerified } + } else { + wasVerified } // Already processed this event. if (note.event != null) return false if (isVerified || justVerify(event)) { - // The blob is on disk now, so the copy in the note drops its content. Without a - // directory to spill to there is nowhere else for it to live, so keep it whole. + // The blob is in the store, so the copy in the note drops its content. If it is + // not — no store, a refused write, undecodable content — the note is the only + // thing holding those bytes, so the event stays whole. val stored = - if (cachePath != null) { + if (spilled) { FileStorageEvent(event.id, event.pubKey, event.createdAt, event.tags, "", event.sig) } else { event diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt index 27cf2884f1..5e60076323 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt @@ -26,14 +26,13 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import java.io.File /** * The handful of things the event cache needs from the application shell around it. * * The cache itself is a plain in-memory store and can run without any of them, so every * member here has a default that is the honest answer for a host that does not provide it - * — an own scope, no NIP-95 spill directory, no relay identity, no stats sink. A front end + * — an own scope, nowhere to spill NIP-95 blobs, no relay identity, no stats sink. A front end * installs its own via [EventCache.appHost]; Android's `Amethyst` does so at startup, * Desktop and unit tests keep the defaults. * @@ -57,10 +56,10 @@ interface LocalCacheHost { val isDebug: Boolean /** - * Directory the NIP-95 `FileStorageEvent` blobs are spilled to, so their bytes can leave - * memory once they are on disk. `null` keeps the event in memory and skips the spill. + * Where NIP-95 `FileStorageEvent` blobs are spilled, so their bytes can leave memory once + * they are stored. `null` keeps the event in memory and skips the spill. */ - val nip95BlobDir: File? + val nip95Blobs: Nip95BlobStore? /** Per-relay counters the anti-spam filter reports duplicate events to. */ val relayStats: RelayStats? @@ -81,7 +80,7 @@ interface LocalCacheHost { companion object Default : LocalCacheHost { override val scope: CoroutineScope by lazy { CoroutineScope(Dispatchers.IO + SupervisorJob()) } override val isDebug = false - override val nip95BlobDir: File? = null + override val nip95Blobs: Nip95BlobStore? = null override val relayStats: RelayStats? = null } } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/SystemFileSystem.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/SystemFileSystem.jvmAndroid.kt new file mode 100644 index 0000000000..2ca484b1cd --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/SystemFileSystem.jvmAndroid.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.amethyst.commons.util + +import okio.FileSystem + +actual val platformFileSystem: FileSystem = FileSystem.SYSTEM From e6c10fca3167721b76514d3553a4fdc1bda44d2b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 15:27:48 +0000 Subject: [PATCH 17/29] refactor(commons): EventCache.filter returns a List, not a SortedSet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit java.util.SortedSet has no common equivalent, and it was in the cache's own signature plus both *ListMatchingFilter observables. The ripple was smaller than the plan assumed: ICacheProvider never declared filter and DesktopLocalCache has no equivalent, so the type was EventCache-local. The result is now a List carrying the same order the SortedSet did — CreatedAtIdHexComparator, newest first. That order is load-bearing rather than incidental: the napplet gateway answers REQs out of this call, and NIP-01 has relays return events newest first. The comparator was also doing something the type change had to preserve. It defines uniqueness by reference, so the sorted set collapsed duplicate references — which a filter repeating a kind produces, since the addressable scan runs once per kind. toSet() before sorting keeps exactly that: Note declares no equals(), so a plain Set de-duplicates by reference too. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../amethyst/commons/model/cache/EventCache.kt | 15 ++++++++++----- .../model/observables/EventListMatchingFilter.kt | 3 +-- .../model/observables/NoteListMatchingFilter.kt | 3 +-- .../observables/EventListMatchingFilterTest.kt | 3 +-- .../observables/NoteListMatchingFilterTest.kt | 3 +-- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt index 4edd119a23..59c2a4a764 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt @@ -422,7 +422,6 @@ import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import java.util.SortedSet /** * The in-memory event store: every `Note`, `User` and `Channel` the app has consumed, plus the @@ -590,7 +589,7 @@ open class EventCache : } } - fun filter(filter: Filter): SortedSet = filter(filter) { true } + fun filter(filter: Filter): List = filter(filter) { true } /** * Every note matching [filter]'s NIP-01 fields that also satisfies [predicate]. @@ -603,7 +602,7 @@ open class EventCache : fun filter( filter: Filter, predicate: (Note) -> Boolean, - ): SortedSet { + ): List { val byKinds = filter.kinds?.filter { it.isAddressable() || it.isReplaceable() } val addressableMatches = @@ -642,14 +641,20 @@ open class EventCache : } } - val all = (addressableMatches + noteMatches).toSortedSet(CreatedAtIdHexComparator) + // toSet() before sorting: a filter that repeats a kind scans that kind twice, and the + // SortedSet this used to return collapsed the repeats. Note declares no equals(), so a + // plain Set is the same reference-identity de-duplication that comparator gave. + // + // The order is the comparator's, newest first, and it is load-bearing: the napplet + // gateway answers REQs out of this, and NIP-01 has relays return events newest first. + val all = (addressableMatches + noteMatches).toSet().sortedWith(CreatedAtIdHexComparator) val limit = filter.limit ?: return all // Sorted first, then cut. Both halves arrive in hash-walk order, so taking before sorting // dropped whichever matches the walk happened to reach last — the newest ones as often as // not — and a query with 200 addressable matches never showed a single regular note. if (all.size <= limit) return all - return all.asSequence().take(limit).toCollection(sortedSetOf(CreatedAtIdHexComparator)) + return all.take(limit) } fun observeNotes(filter: Filter): Flow> = diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt index c6373a1267..c46ba85e8f 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt @@ -26,7 +26,6 @@ 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.filters.Filter -import java.util.SortedSet import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentSkipListSet @@ -69,7 +68,7 @@ import java.util.concurrent.ConcurrentSkipListSet */ class EventListMatchingFilter( private val filter: Filter, - private val atOnce: (filter: Filter) -> SortedSet, + private val atOnce: (filter: Filter) -> List, private val update: (List) -> Unit, ) : Observable { /** A note plus the sort key captured at insertion time, so ordering never depends on mutable state. */ diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt index d2962053fd..35d395f626 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt @@ -26,7 +26,6 @@ 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.filters.Filter -import java.util.SortedSet import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentSkipListSet @@ -68,7 +67,7 @@ import java.util.concurrent.ConcurrentSkipListSet */ class NoteListMatchingFilter( private val filter: Filter, - private val atOnce: (filter: Filter) -> SortedSet, + private val atOnce: (filter: Filter) -> List, private val update: (List) -> Unit, ) : Observable { /** A note plus the sort key captured at insertion time, so ordering never depends on mutable state. */ diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilterTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilterTest.kt index b28699e362..6e46771881 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilterTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilterTest.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.utils.EventFactory -import java.util.TreeSet import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicReference import kotlin.concurrent.thread @@ -65,7 +64,7 @@ class EventListMatchingFilterTest { sink: (List) -> Unit, ) = EventListMatchingFilter( filter = withFilter, - atOnce = { TreeSet(CreatedAtIdHexComparator) }, + atOnce = { emptyList() }, update = sink, ) diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilterTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilterTest.kt index a77efc8318..ba366ecb62 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilterTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilterTest.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.utils.EventFactory -import java.util.TreeSet import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicReference import kotlin.concurrent.thread @@ -67,7 +66,7 @@ class NoteListMatchingFilterTest { sink: (List) -> Unit, ) = NoteListMatchingFilter( filter = withFilter, - atOnce = { TreeSet(CreatedAtIdHexComparator) }, + atOnce = { emptyList() }, update = sink, ) From ee636dcf17e976a6332da6b7e4bba4f0aff5b895 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 15:32:57 +0000 Subject: [PATCH 18/29] refactor(commons): make the matching-filter observables expect/actual MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These were the last two files standing between EventCache and commonMain, and the only ones whose blocker is a concurrency design rather than a type. Both keep one entry per idHex under concurrent ingest by doing every write to a ConcurrentSkipListSet inside that key's ConcurrentHashMap.compute critical section — per-key striping, no lock — and de-duplicating the weakly-consistent iterator in the snapshot. Kotlin/Native has neither primitive, and this is not a case where the pieces can be swapped one at a time. quartz's ConcurrentMap offers getOrPut, which matches new(), but remove() needs the sorted-set removal to happen inside the critical section: moving it outside lets a concurrent re-add insert a comparator-equal entry that the later remove would take out instead, dropping the note from the list for good. A copy-on-write compute cannot fix that either, since its CAS retry may run the lambda more than once and this lambda has side effects. So the declaration moves to commonMain and today's implementation stays exactly as it is, byte for byte, as the jvmAndroid actual. iOS throws, as LargeSoftCache already does, with the contract it would need written down. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../observables/EventListMatchingFilter.kt | 53 +++++++++++++++++++ .../observables/NoteListMatchingFilter.kt | 53 +++++++++++++++++++ .../EventListMatchingFilter.ios.kt | 52 ++++++++++++++++++ .../observables/NoteListMatchingFilter.ios.kt | 52 ++++++++++++++++++ ... => EventListMatchingFilter.jvmAndroid.kt} | 8 +-- ...t => NoteListMatchingFilter.jvmAndroid.kt} | 8 +-- 6 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt create mode 100644 commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.ios.kt create mode 100644 commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.ios.kt rename commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/{EventListMatchingFilter.kt => EventListMatchingFilter.jvmAndroid.kt} (97%) rename commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/{NoteListMatchingFilter.kt => NoteListMatchingFilter.jvmAndroid.kt} (97%) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt new file mode 100644 index 0000000000..921cad683e --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.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.commons.model.observables + +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter + +/** + * A relay-like list of the events matching a filter, newest first, re-emitting on addressable updates. + * + * The declaration is shared so the event cache can live in `commonMain`; the implementation is + * not. One entry per idHex under concurrent ingest is kept by doing every write to the sorted + * set inside that key's `ConcurrentHashMap.compute` critical section — per-key striping, no + * lock — over a `ConcurrentSkipListSet` whose weakly-consistent iterator the snapshot + * de-duplicates. Kotlin/Native has neither primitive, and the argument for why this is correct + * (spelled out on the jvmAndroid actual) does not survive being reassembled out of weaker ones: + * a copy-on-write `compute` may re-run its lambda, and this lambda has side effects. + * + * So iOS has no implementation yet and every member throws — see `EventListMatchingFilter.ios.kt`. + */ +expect class EventListMatchingFilter( + filter: Filter, + atOnce: (filter: Filter) -> List, + update: (List) -> Unit, +) : Observable { + override fun new( + event: Event, + note: Note, + ) + + override fun remove(note: Note) + + fun init() +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt new file mode 100644 index 0000000000..f3811c54ec --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.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.commons.model.observables + +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter + +/** + * A relay-like list of the notes matching a filter, newest first, that only grows. + * + * The declaration is shared so the event cache can live in `commonMain`; the implementation is + * not. One entry per idHex under concurrent ingest is kept by doing every write to the sorted + * set inside that key's `ConcurrentHashMap.compute` critical section — per-key striping, no + * lock — over a `ConcurrentSkipListSet` whose weakly-consistent iterator the snapshot + * de-duplicates. Kotlin/Native has neither primitive, and the argument for why this is correct + * (spelled out on the jvmAndroid actual) does not survive being reassembled out of weaker ones: + * a copy-on-write `compute` may re-run its lambda, and this lambda has side effects. + * + * So iOS has no implementation yet and every member throws — see `NoteListMatchingFilter.ios.kt`. + */ +expect class NoteListMatchingFilter( + filter: Filter, + atOnce: (filter: Filter) -> List, + update: (List) -> Unit, +) : Observable { + override fun new( + event: Event, + note: Note, + ) + + override fun remove(note: Note) + + fun init() +} diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.ios.kt new file mode 100644 index 0000000000..37831da78e --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.ios.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.commons.model.observables + +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter + +actual class EventListMatchingFilter actual constructor( + filter: Filter, + atOnce: (filter: Filter) -> List, + update: (List) -> Unit, +) : Observable { + actual override fun new( + event: Event, + note: Note, + ) { + notYetImplemented() + } + + actual override fun remove(note: Note) { + notYetImplemented() + } + + actual fun init() { + notYetImplemented() + } +} + +private fun notYetImplemented(): Nothing = + throw NotImplementedError( + "The matching-filter observables have no iOS implementation yet: they need a per-key " + + "atomic section over a sorted concurrent set.", + ) diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.ios.kt new file mode 100644 index 0000000000..be2bb3e51f --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.ios.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.commons.model.observables + +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter + +actual class NoteListMatchingFilter actual constructor( + filter: Filter, + atOnce: (filter: Filter) -> List, + update: (List) -> Unit, +) : Observable { + actual override fun new( + event: Event, + note: Note, + ) { + notYetImplemented() + } + + actual override fun remove(note: Note) { + notYetImplemented() + } + + actual fun init() { + notYetImplemented() + } +} + +private fun notYetImplemented(): Nothing = + throw NotImplementedError( + "The matching-filter observables have no iOS implementation yet: they need a per-key " + + "atomic section over a sorted concurrent set.", + ) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.jvmAndroid.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.jvmAndroid.kt index c46ba85e8f..0e5a6ec4cc 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.jvmAndroid.kt @@ -66,7 +66,7 @@ import java.util.concurrent.ConcurrentSkipListSet * emitted list must never carry a duplicate id — a LazyColumn keyed on it would * crash — so [snapshot] deduplicates by the stable idHex as it materializes. */ -class EventListMatchingFilter( +actual class EventListMatchingFilter actual constructor( private val filter: Filter, private val atOnce: (filter: Filter) -> List, private val update: (List) -> Unit, @@ -95,7 +95,7 @@ class EventListMatchingFilter( } @Suppress("UNCHECKED_CAST") - override fun new( + actual override fun new( event: Event, note: Note, ) { @@ -134,7 +134,7 @@ class EventListMatchingFilter( } @Suppress("UNCHECKED_CAST") - override fun remove(note: Note) { + actual override fun remove(note: Note) { var removed = false byId.compute(note.idHex) { _, existing -> if (existing != null) { @@ -147,7 +147,7 @@ class EventListMatchingFilter( } @Suppress("UNCHECKED_CAST") - fun init() { + actual fun init() { sorted.clear() byId.clear() atOnce(filter).forEach { note -> diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.jvmAndroid.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.jvmAndroid.kt index 35d395f626..f3466a548d 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.jvmAndroid.kt @@ -65,7 +65,7 @@ import java.util.concurrent.ConcurrentSkipListSet * a duplicate idHex — the LazyColumn keyed on it would crash — so [snapshot] * deduplicates by idHex as it materializes. */ -class NoteListMatchingFilter( +actual class NoteListMatchingFilter actual constructor( private val filter: Filter, private val atOnce: (filter: Filter) -> List, private val update: (List) -> Unit, @@ -94,7 +94,7 @@ class NoteListMatchingFilter( return Entry(note, note.createdAt() ?: Long.MIN_VALUE, event?.id ?: note.idHex) } - override fun new( + actual override fun new( event: Event, note: Note, ) { @@ -122,7 +122,7 @@ class NoteListMatchingFilter( update(snapshot()) } - override fun remove(note: Note) { + actual override fun remove(note: Note) { // Remove from [sorted] atomically with releasing the idHex slot. var removed = false byId.compute(note.idHex) { _, existing -> @@ -135,7 +135,7 @@ class NoteListMatchingFilter( if (removed) update(snapshot()) } - fun init() { + actual fun init() { sorted.clear() byId.clear() atOnce(filter).forEach { note -> From f2e4454e8e98514e8f34715efd6d0a48a154d97e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 15:43:23 +0000 Subject: [PATCH 19/29] refactor(commons): move the event cache into commonMain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EventCache, LocalCache, LocalCacheHost, AntiSpamFilter, CachePruner, CacheSearch and OnchainZapResolver are now shared; commons/jvmAndroid keeps only the LargeSoftCache actual. The three blockers were cleared in the preceding commits. What the move itself turned up was the part no import grep could see, since these are JVM APIs that need no import: @Synchronized and synchronized {} became KmpLock, @Volatile became kotlin.concurrent.Volatile, System.nanoTime became TimeSource.Monotonic, HashMap.merge became a local mergeMax, and Dispatchers.IO just needed kotlinx.coroutines.IO imported. Two references could not follow the file. The one dateFormatter call was a log line and now prints the raw created_at. LnurlEndpointCache is a quartz jvmAndroid singleton the outbound-zap resolver fills, so the zap-receipt validation reads it through a new LocalCacheHost.lnurlEndpoint port; its default null is "not cached", which is what a cache miss already meant — the receipt takes the resolver path instead. This is not iOS support. iOS compiles the cache now; its store and its two observables still throw there. The gap is three named files rather than a module boundary. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../amethyst/AmethystLocalCacheHost.kt | 6 ++ .../2026-08-30-commons-migration-sweep.md | 58 +++++++++++++++++++ .../commons/model/cache/AntiSpamFilter.kt | 58 ++++++++++--------- .../commons/model/cache/CachePruner.kt | 18 +++++- .../commons/model/cache/CacheSearch.kt | 0 .../commons/model/cache/EventCache.kt | 19 +++--- .../commons/model/cache/LocalCache.kt | 0 .../commons/model/cache/LocalCacheHost.kt | 12 ++++ .../nipBCOnchainZaps/OnchainZapResolver.kt | 0 9 files changed, 134 insertions(+), 37 deletions(-) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/model/cache/AntiSpamFilter.kt (84%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt (97%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt (100%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt (99%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCache.kt (100%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt (87%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt (100%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt index b935b7a401..48b2b8e81c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt @@ -28,6 +28,8 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread 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.nip57Zaps.validate.LnurlEndpointCache +import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointInfo import kotlinx.coroutines.CoroutineScope /** @@ -54,5 +56,9 @@ class AmethystLocalCacheHost( override fun relaySelfPubKey(relay: NormalizedRelayUrl): HexKey? = modules.nip11Cache.getFromCache(relay).self + // The zap path's LNURL cache is a quartz-side singleton the outbound-zap resolver + // fills; it is jvmAndroid-only, which is why the cache reads it through here. + override fun lnurlEndpoint(lnurlpUrl: String): LnurlEndpointInfo? = LnurlEndpointCache.get(lnurlpUrl) + override fun assertNotMainThread() = checkNotInMainThread() } diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index 5619e09297..b3dd5e48fa 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1161,3 +1161,61 @@ Verified: `:commons:compileCommonMainKotlinMetadata`, `:commons:compileIosMainKotlinMetadata`, `:commons:jvmTest` (incl. the moved `NwcPaymentTrackerTest`, 4 tests), `:desktopApp:test`, `:cli:test`, `:amethyst:compileFdroidDebugKotlin`, `DvmHeartbeatTest`, `spotlessCheck`. + +### Steps 3–5 shipped: the cache group is `commonMain` + +`commons/src/jvmAndroid/…/model/cache/` now holds exactly one file, the +`LargeSoftCache` actual. Everything else — `EventCache` (4k lines), +`LocalCache`, `LocalCacheHost`, `AntiSpamFilter`, `CachePruner`, `CacheSearch`, +`OnchainZapResolver` — is shared. + +**The NIP-95 sink.** `nip95BlobDir: File?` became `nip95Blobs: Nip95BlobStore?`, +because a directory is not what the cache needs — somewhere to put bytes and a +way to ask whether they are there already is. `FileSystemNip95BlobStore` is the +okio-backed one, and okio was already a commons dependency. Its constructor +takes a plain path string so Android needs no okio of its own, and +`platformFileSystem` is a two-line expect/actual because okio declares +`FileSystem.SYSTEM` per platform. + +Two bugs surfaced while rewriting that block. `decode()` returns `ByteArray?` +and went straight into `FileOutputStream.write` — a platform type, so a null +would have thrown past the `IOException` catch. And the note's copy dropped its +content whenever a directory existed, *including* right after a failed write, +losing the only copy of those bytes; content is now dropped only when the blob +really is stored. + +**`SortedSet`.** `EventCache.filter` returns a `List` in the comparator's +order, newest first. That order is load-bearing (the napplet gateway answers +REQs from it). The comparator also defines uniqueness by reference, so the +sorted set was collapsing the duplicate references a filter with a repeated +kind produces; `toSet()` before sorting keeps exactly that, since `Note` +declares no `equals()`. + +**The observables.** `NoteListMatchingFilter` / `EventListMatchingFilter` are +now `expect`/`actual` with today's implementation untouched as the jvmAndroid +actual and a throwing iOS stub. Their concurrency — every sorted-set write +inside that key's `ConcurrentHashMap.compute` critical section — is not +reassemblable from quartz's KMP primitives: `getOrPut` covers `new()`, but +`remove()` needs the sorted-set removal *inside* the section, or a concurrent +re-add inserts a comparator-equal entry that the later removal takes out +instead, dropping the note for good. A copy-on-write `compute` cannot stand in +either, since its CAS retry may run a side-effecting lambda twice. + +**What the move itself turned up**, none of it visible to an import grep: +`@Synchronized` / `@Volatile` / `synchronized {}` (→ `KmpLock`, +`kotlin.concurrent.Volatile`), `System.nanoTime` (→ `TimeSource.Monotonic`), +`HashMap.merge` (→ a local `mergeMax`), `Dispatchers.IO` (just needs +`import kotlinx.coroutines.IO`), the one `dateFormatter` log line (now logs the +raw `created_at`), and `LnurlEndpointCache` — a quartz **jvmAndroid** singleton, +reached through a new `LocalCacheHost.lnurlEndpoint` port whose default `null` +means "cold cache", which is what a miss already meant. + +**Still true, and worth repeating:** commonMain is not iOS support. iOS now +compiles the cache, and the cache's store and its two observables throw there. +The remaining gap is three named files, not a module boundary. + +Verified: `:commons:compileCommonMainKotlinMetadata`, +`:commons:compileIosMainKotlinMetadata`, `:commons:verifyKmpPurity`, +`:commons:jvmTest`, `:desktopApp:test`, `:cli:test`, +`:amethyst:compileFdroidDebugKotlin`, `:amethyst:testPlayDebugUnitTest`, +`spotlessCheck`. diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/AntiSpamFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/AntiSpamFilter.kt similarity index 84% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/AntiSpamFilter.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/AntiSpamFilter.kt index d4a91650f4..545e73cd6d 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/AntiSpamFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/AntiSpamFilter.kt @@ -21,7 +21,9 @@ package com.vitorpamplona.amethyst.commons.model.cache import androidx.collection.LruCache +import com.vitorpamplona.amethyst.commons.util.KmpLock import com.vitorpamplona.amethyst.commons.util.njumpLink +import com.vitorpamplona.amethyst.commons.util.withLock import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event @@ -160,39 +162,43 @@ class AntiSpamFilter( return false } - @Synchronized + // Was @Synchronized, which is a JVM-only annotation: same mutual exclusion, spelled + // for every target. Only this method touches spamMessages for a given hash. + private val offenderLock = KmpLock() + private fun logOffender( hashCode: Int, event: Event, - ): Spammer { - val spammer = spamMessages.get(hashCode) + ): Spammer = + offenderLock.withLock { + val spammer = spamMessages.get(hashCode) - return if (spammer == null) { - val newSpammer = - if (event is AddressableEvent) { - Spammer( - pubkeyHex = event.pubKey, - duplicatedEventIds = setOf(), - duplicatedEventAddresses = setOfNotNull(recentAddressables[hashCode], event.address()), - ) - } else { - Spammer( - pubkeyHex = event.pubKey, - duplicatedEventIds = setOfNotNull(recentEventIds[hashCode], event.id), - duplicatedEventAddresses = setOf(), - ) - } - spamMessages.put(hashCode, newSpammer) - newSpammer - } else { - if (event is AddressableEvent) { - spammer.duplicatedEventAddresses += event.address() + if (spammer == null) { + val newSpammer = + if (event is AddressableEvent) { + Spammer( + pubkeyHex = event.pubKey, + duplicatedEventIds = setOf(), + duplicatedEventAddresses = setOfNotNull(recentAddressables[hashCode], event.address()), + ) + } else { + Spammer( + pubkeyHex = event.pubKey, + duplicatedEventIds = setOfNotNull(recentEventIds[hashCode], event.id), + duplicatedEventAddresses = setOf(), + ) + } + spamMessages.put(hashCode, newSpammer) + newSpammer } else { - spammer.duplicatedEventIds += event.id + if (event is AddressableEvent) { + spammer.duplicatedEventAddresses += event.address() + } else { + spammer.duplicatedEventIds += event.id + } + spammer } - spammer } - } val flowSpam = MutableStateFlow(AntiSpamState(this)) } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt index 714cf8998f..8f26268712 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt @@ -214,12 +214,12 @@ class CachePruner( is BaseDMGroupEvent -> if (giftWrapFloor != null) { val outerUntil = note.rumorHost?.createdAt ?: ev.createdAt - if (outerUntil < giftWrapFloor) note.relays.forEach { giftWrapPruned.merge(it, outerUntil, ::maxOf) } + if (outerUntil < giftWrapFloor) note.relays.forEach { giftWrapPruned.mergeMax(it, outerUntil) } } is PrivateDmEvent -> { val until = ev.createdAt - if (accountNip04Floor != null && until < accountNip04Floor) note.relays.forEach { accountNip04Pruned.merge(it, until, ::maxOf) } - if (roomNip04Floor != null && until < roomNip04Floor) note.relays.forEach { roomNip04Pruned.merge(it, until, ::maxOf) } + if (accountNip04Floor != null && until < accountNip04Floor) note.relays.forEach { accountNip04Pruned.mergeMax(it, until) } + if (roomNip04Floor != null && until < roomNip04Floor) note.relays.forEach { roomNip04Pruned.mergeMax(it, until) } } } @@ -500,3 +500,15 @@ class CachePruner( println("PRUNE: ${toBeRemoved.size} messages removed because they were Hidden") } } + +/** + * `Map.merge(key, value, ::maxOf)` spelled for every target — the JVM's own merge is not in the + * common stdlib. Keeps the highest value seen for [relay]. + */ +private fun MutableMap.mergeMax( + relay: NormalizedRelayUrl, + value: Long, +) { + val existing = this[relay] + this[relay] = if (existing == null) value else maxOf(existing, value) +} diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt similarity index 99% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt index 59c2a4a764..5ffd5bb72f 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt @@ -58,7 +58,8 @@ import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList import com.vitorpamplona.amethyst.commons.model.redirectStrayRelayGroupContent import com.vitorpamplona.amethyst.commons.service.BundledInsert import com.vitorpamplona.amethyst.commons.service.nwc.NwcPaymentTracker -import com.vitorpamplona.amethyst.commons.util.dateFormatter +import com.vitorpamplona.amethyst.commons.util.KmpLock +import com.vitorpamplona.amethyst.commons.util.withLock import com.vitorpamplona.quartz.buzz.aeEngrams.EngramEvent import com.vitorpamplona.quartz.buzz.agentProfiles.AgentProfileEvent import com.vitorpamplona.quartz.buzz.amTurnMetrics.AgentTurnMetricEvent @@ -320,7 +321,6 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.validate.LnZapReceiptValidator -import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointCache import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointResolver import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlForm import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent @@ -413,6 +413,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.cache.LargeCache import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -422,6 +423,8 @@ import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlin.concurrent.Volatile +import kotlin.time.TimeSource /** * The in-memory event store: every `Note`, `User` and `Channel` the app has consumed, plus the @@ -509,7 +512,7 @@ open class EventCache : val mintDirectory = MintDirectoryIndex() @Volatile private var mintDirectoryBackfilled = false - private val mintDirectoryBackfillLock = Any() + private val mintDirectoryBackfillLock = KmpLock() /** * Sweeps `notes` + `addressables` for any NIP-87 / NIP-61 event the @@ -525,7 +528,7 @@ open class EventCache : */ fun ensureMintDirectoryBackfilled() { if (mintDirectoryBackfilled) return - synchronized(mintDirectoryBackfillLock) { + mintDirectoryBackfillLock.withLock { if (mintDirectoryBackfilled) return runCatching { notes.forEach { _, note -> note.event?.let(::updateMintIndex) } @@ -2678,7 +2681,7 @@ open class EventCache : // stays in cache as a visible artifact but contributes 0 to zap totals. val recipientLnurl = recipientLnurl(event) val recipientLnurlpUrl = recipientLnurl?.let { LnurlForm.toUrl(it) } - val cachedInfo = recipientLnurlpUrl?.let { LnurlEndpointCache.get(it) } + val cachedInfo = recipientLnurlpUrl?.let { appHost.lnurlEndpoint(it) } val author = getOrCreateUser(event.pubKey) val repliesTo = computeReplyTo(event) @@ -3334,9 +3337,9 @@ open class EventCache : val meter = verifyMeter if (meter == null) return justVerifyInner(event) - val start = System.nanoTime() + val start = TimeSource.Monotonic.markNow() val valid = justVerifyInner(event) - meter(System.nanoTime() - start, valid) + meter(start.elapsedNow().inWholeNanoseconds, valid) return valid } @@ -3346,7 +3349,7 @@ open class EventCache : 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}" } + Log.w("Event Verification Failed") { "Kind: ${event.kind} created at ${event.createdAt} with message ${e.message}" } } false } else { diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCache.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCache.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt similarity index 87% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt index 5e60076323..12d33d9fe5 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt @@ -23,8 +23,10 @@ package com.vitorpamplona.amethyst.commons.model.cache 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.nip57Zaps.validate.LnurlEndpointInfo import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.SupervisorJob /** @@ -70,6 +72,16 @@ interface LocalCacheHost { */ fun relaySelfPubKey(relay: NormalizedRelayUrl): HexKey? = null + /** + * LNURL-pay endpoint metadata the shell has already resolved for [lnurlpUrl], used to + * validate a zap receipt's signer against the provider's `nostrPubkey` (NIP-57 Appendix F). + * + * `null` is "not cached", which is what a cache miss already means here: the receipt takes + * the slower resolver path instead. A host that caches nothing therefore behaves like a + * cold cache, not like a broken one. + */ + fun lnurlEndpoint(lnurlpUrl: String): LnurlEndpointInfo? = null + /** * Throws if called on the platform's main thread. Signature verification and the cache * sweeps are far too slow to run there. A no-op by default and on release builds. diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nipBCOnchainZaps/OnchainZapResolver.kt From 61ceaa728f7166124b8ab768ccc3d281ebb2b810 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 16:28:59 +0000 Subject: [PATCH 20/29] test(amethyst): hold the DVM announcement fixtures against the weak cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit theUngatedAnnouncementScanKeepsDvmsTheGateWouldHide failed on CI at the "dvm-x must still be sourced" assertion. The consume and the scan are adjacent statements in one method, so nothing between them can have polluted the cache; what the failure says is that the whole addressables scan came back empty, which is how a LargeSoftCache reports its WeakReferences being cleared. The test consumed three announcements and held a strong reference to none of them. LargeCacheAddressableFilterTest already documents this exact trap for exactly this store. So the fixture now keeps the Notes it expects the scan to find, and a forced GC sits between the consume and the scan so that stays proven rather than assumed. Two locals, `alive` and `subscriptionApp`, were built and never consumed, which left "subscription apps are not content-discovery DVMs" asserting the absence of something that was never there. They are consumed now, so that assertion means what it says. I could not reproduce the clearing locally — a probe shows the note is still retained after two full GCs here, so the CI heap is what makes the difference. The fix removes the dependence either way. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../amethyst/model/DvmHeartbeatTest.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/DvmHeartbeatTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/DvmHeartbeatTest.kt index 22fbbb4e67..cb11cc64d2 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/DvmHeartbeatTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/DvmHeartbeatTest.kt @@ -160,9 +160,7 @@ class DvmHeartbeatTest { sig = "cc".repeat(64), ) - LocalCache.justConsume(appDef("dvm-x"), null, true) - LocalCache.justConsume(nonDiscoveryApp, null, true) - val consumed = + val secondSubscriptionApp = AppDefinitionEvent( id = "c2".repeat(32), pubKey = appDefPubKey, @@ -171,7 +169,18 @@ class DvmHeartbeatTest { content = """{"name":"Paid2","subscription":true}""", sig = "cc".repeat(64), ) - LocalCache.justConsume(consumed, null, true) + + // `addressables` holds notes by WeakReference, so a GC between consuming these and + // scanning for them clears the lot and the scan returns nothing — which is exactly how + // this failed on CI, where the heap is tighter than a dev box's. A fixture has to hold + // what it expects to find, the same discipline LargeCacheAddressableFilterTest spells + // out; the forced GC below is what keeps that honest rather than assumed. + val held = + listOf(alive, appDef("dvm-x"), subscriptionApp, nonDiscoveryApp, secondSubscriptionApp) + .onEach { LocalCache.justConsume(it, null, true) } + .map { LocalCache.getOrCreateNote(it) } + + System.gc() val scanned = LocalCache.cachedDvmAnnouncements() @@ -180,5 +189,6 @@ class DvmHeartbeatTest { assertFalse("k=9999 apps are not content-discovery DVMs", scanned.any { it.dTag() == "other" }) assertEquals("newest-first so the cap keeps the most relevant announcements", scanned.sortedByDescending { it.createdAt }, scanned) assertTrue("capped", scanned.size <= 100) + assertEquals("the fixture notes must stay reachable for the whole test", 5, held.size) } } From c25cf6524f20a1a15cbf7f5d8c90536fbe32e1dc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 17:57:14 +0000 Subject: [PATCH 21/29] refactor(commons): make the list observables lock-free and common MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both matching-filter observables were a ConcurrentSkipListSet coordinated with a ConcurrentHashMap.compute per-key critical section — lock-free, and with no Kotlin/Native equivalent, which is what forced the expect/actual and the two throwing iOS stubs. Copy-on-write over an atomic reference gives the same lock-freedom with none of that: one immutable State behind an AtomicReference, each mutation building the next state and publishing it with a compare-and-set, retrying if another thread won. It is the shape quartz already uses for its native ConcurrentMap actual, and it compiles everywhere, so the expect/actual, the jvmAndroid actuals and the iOS stubs are all gone. Two hand-written workarounds go with them. The emitted snapshot no longer de-duplicates by idHex — that pass existed only because a weakly-consistent skip-list iterator can transiently surface a key twice, and a reader of an immutable state cannot. And emitting is now one map over the published state rather than a walk of a concurrent structure plus a HashSet, so the per-event cost drops even though the copy is O(n): the previous code already paid O(n) per emit, on top of the insert. The sort key is still snapshotted into an immutable Entry at insertion, so a replaceable event mutating created_at on the same AddressableNote instance still cannot move an entry — the test that pins that behaviour, and both 8 thread x 5000 iteration concurrency tests, pass unchanged. ensureMintDirectoryBackfilled loses its lock too: a CAS on the claim flag means exactly one caller sweeps, and a second returns immediately instead of blocking for the length of a full cache scan. It may see a partially filled index, which is what it already saw before any backfill ran. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../commons/model/cache/EventCache.kt | 26 +-- .../observables/EventListMatchingFilter.kt | 156 ++++++++++++++-- .../observables/NoteListMatchingFilter.kt | 147 +++++++++++++-- .../EventListMatchingFilter.ios.kt | 52 ------ .../observables/NoteListMatchingFilter.ios.kt | 52 ------ .../EventListMatchingFilter.jvmAndroid.kt | 167 ------------------ .../NoteListMatchingFilter.jvmAndroid.kt | 154 ---------------- 7 files changed, 285 insertions(+), 469 deletions(-) delete mode 100644 commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.ios.kt delete mode 100644 commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.ios.kt delete mode 100644 commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.jvmAndroid.kt delete mode 100644 commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.jvmAndroid.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt index 5ffd5bb72f..85ca2ebe0e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt @@ -58,8 +58,6 @@ import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList import com.vitorpamplona.amethyst.commons.model.redirectStrayRelayGroupContent import com.vitorpamplona.amethyst.commons.service.BundledInsert import com.vitorpamplona.amethyst.commons.service.nwc.NwcPaymentTracker -import com.vitorpamplona.amethyst.commons.util.KmpLock -import com.vitorpamplona.amethyst.commons.util.withLock import com.vitorpamplona.quartz.buzz.aeEngrams.EngramEvent import com.vitorpamplona.quartz.buzz.agentProfiles.AgentProfileEvent import com.vitorpamplona.quartz.buzz.amTurnMetrics.AgentTurnMetricEvent @@ -424,6 +422,8 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlin.concurrent.Volatile +import kotlin.concurrent.atomics.AtomicBoolean +import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.time.TimeSource /** @@ -511,8 +511,8 @@ open class EventCache : */ val mintDirectory = MintDirectoryIndex() - @Volatile private var mintDirectoryBackfilled = false - private val mintDirectoryBackfillLock = KmpLock() + @OptIn(ExperimentalAtomicApi::class) + private val mintDirectoryBackfilled = AtomicBoolean(false) /** * Sweeps `notes` + `addressables` for any NIP-87 / NIP-61 event the @@ -526,15 +526,17 @@ open class EventCache : * scan failure is swallowed so the index stays usable even if the * cache is in an unexpected state. */ + @OptIn(ExperimentalAtomicApi::class) fun ensureMintDirectoryBackfilled() { - if (mintDirectoryBackfilled) return - mintDirectoryBackfillLock.withLock { - if (mintDirectoryBackfilled) return - runCatching { - notes.forEach { _, note -> note.event?.let(::updateMintIndex) } - addressables.forEach { _, note -> note.event?.let(::updateMintIndex) } - } - mintDirectoryBackfilled = true + // Claim the sweep with a CAS rather than a lock: exactly one caller gets true and does + // the work. A second caller returns immediately instead of blocking for the length of a + // full cache scan — it may see a partially filled index, which is the same thing it sees + // before any backfill runs, and one relay round-trip later it does not. + if (!mintDirectoryBackfilled.compareAndSet(false, true)) return + + runCatching { + notes.forEach { _, note -> note.event?.let(::updateMintIndex) } + addressables.forEach { _, note -> note.event?.let(::updateMintIndex) } } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt index 921cad683e..dba6c0eb20 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt @@ -18,36 +18,160 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +@file:OptIn(ExperimentalAtomicApi::class) + package com.vitorpamplona.amethyst.commons.model.observables +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +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.filters.Filter +import kotlin.concurrent.atomics.AtomicReference +import kotlin.concurrent.atomics.ExperimentalAtomicApi /** - * A relay-like list of the events matching a filter, newest first, re-emitting on addressable updates. + * Creates a list of events (regular and addressable), sorted by created_at, that is updated + * every time a new matching event is received — INCLUDING newer versions of addressables, whose + * refreshed content re-emits (the entry keeps its captured position; consumers that care about + * order re-sort downstream). * - * The declaration is shared so the event cache can live in `commonMain`; the implementation is - * not. One entry per idHex under concurrent ingest is kept by doing every write to the sorted - * set inside that key's `ConcurrentHashMap.compute` critical section — per-key striping, no - * lock — over a `ConcurrentSkipListSet` whose weakly-consistent iterator the snapshot - * de-duplicates. Kotlin/Native has neither primitive, and the argument for why this is correct - * (spelled out on the jvmAndroid actual) does not survive being reassembled out of weaker ones: - * a copy-on-write `compute` may re-run its lambda, and this lambda has side effects. + * There is exactly one [Note] instance per id/address (the cache owns their creation), so + * uniqueness is a non-issue in principle — except a note's sort key is mutable: a newer + * replaceable event swaps the event on the SAME [AddressableNote] instance, changing its + * created_at in place. Anything ordered on that live value cannot survive it. So the sort key is + * snapshotted into an immutable [Entry] when the note first enters and never read live again. * - * So iOS has no implementation yet and every member throws — see `EventListMatchingFilter.ios.kt`. + * **Lock-free, by copy-on-write.** Observer callbacks fire concurrently from several consume + * threads (relay ingest + UI-side justConsume) and this observer is used everywhere, so it takes + * no lock: the whole list lives in one immutable [State] behind an [AtomicReference], and every + * mutation builds the next state and publishes it with a compare-and-set, retrying if another + * thread won the race. Same shape quartz uses for its native `ConcurrentMap`. + * + * Two things fall out of that, both previously paid for by hand: + * - a reader never sees a torn list, so the emitted snapshot needs no de-duplication pass — + * one entry per idHex holds by construction, since [State.ids] is what gates insertion; + * - the emitted list IS the published state, so emitting costs one map rather than a walk of a + * concurrent structure plus a `HashSet` to strip the duplicates that walk could surface. + * + * The copy is O(n) per write, which is what the previous per-emit snapshot already cost. */ -expect class EventListMatchingFilter( - filter: Filter, - atOnce: (filter: Filter) -> List, - update: (List) -> Unit, +class EventListMatchingFilter( + private val filter: Filter, + private val atOnce: (filter: Filter) -> List, + private val update: (List) -> Unit, ) : Observable { + /** A note plus the sort key captured at insertion time, so ordering never depends on mutable state. */ + private class Entry( + val note: Note, + val createdAt: Long, + val id: HexKey, + ) + + /** One immutable version of the list. [ids] is the membership gate, keyed by the stable idHex. */ + private inner class State( + val entries: List, + val ids: Set, + ) { + @Suppress("UNCHECKED_CAST") + fun events(): List = entries.mapNotNull { it.note.event as? T } + } + + // created_at descending, id ascending as a stable tiebreak. Both fields are + // immutable snapshots, so an Entry never moves once inserted. + private val order = + Comparator { a, b -> + val byCreatedAt = b.createdAt.compareTo(a.createdAt) + if (byCreatedAt != 0) byCreatedAt else a.id.compareTo(b.id) + } + + private val state = AtomicReference(State(emptyList(), emptySet())) + + private fun entryFor(note: Note): Entry { + // A null event (unresolved note) sorts last, matching CreatedAtIdHexComparator. + val event = note.event + return Entry(note, note.createdAt() ?: Long.MIN_VALUE, event?.id ?: note.idHex) + } + + private fun State.plus( + entry: Entry, + limit: Int?, + ): State { + val found = entries.binarySearch(entry, order) + val at = if (found < 0) -found - 1 else found + + val grown = ArrayList(entries.size + 1) + grown.addAll(entries.subList(0, at)) + grown.add(entry) + grown.addAll(entries.subList(at, entries.size)) + + if (limit == null || grown.size <= limit) return State(grown, ids + entry.note.idHex) + + // Over the limit: drop the oldest, which sorts last. That can be the entry just + // inserted, and then it is simply not listed — as the previous pollLast() did. + val dropped = grown.removeAt(grown.size - 1) + return State(grown, ids + entry.note.idHex - dropped.note.idHex) + } + + private fun State.minus(idHex: HexKey): State = State(entries.filterNot { it.note.idHex == idHex }, ids - idHex) + override fun new( event: Event, note: Note, - ) + ) { + if (event is AddressableEvent && note !is AddressableNote) { + // The "version" note (a regular note holding an addressable event) is never + // stored — the AddressableNote is. Re-emit if that addressable is already + // listed, so consumers pick up the refreshed content read live off the note. + val current = state.load() + if (event.address().toValue() in current.ids) update(current.events()) + return + } - override fun remove(note: Note) + if (!filter.match(event)) return - fun init() + val limit = filter.limit + while (true) { + val current = state.load() + // Already listed: the entry keeps its captured position — re-sorting it would + // mean a remove plus an add, and two entries for one note would transiently + // coexist and read the same live event twice. Re-emit so consumers see the + // refreshed content. + if (note.idHex in current.ids) { + update(current.events()) + return + } + + val next = current.plus(entryFor(note), limit) + if (state.compareAndSet(current, next)) { + update(next.events()) + return + } + } + } + + override fun remove(note: Note) { + while (true) { + val current = state.load() + if (note.idHex !in current.ids) return + + val next = current.minus(note.idHex) + if (state.compareAndSet(current, next)) { + update(next.events()) + return + } + } + } + + fun init() { + // The cache query behind [atOnce] has already applied the filter's limit, so this + // inserts what it is given, exactly as the previous implementation did. + var fresh = State(emptyList(), emptySet()) + atOnce(filter).forEach { note -> + if (note.idHex !in fresh.ids) fresh = fresh.plus(entryFor(note), null) + } + state.store(fresh) + update(fresh.events()) + } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt index f3811c54ec..bd8603729b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt @@ -18,36 +18,151 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +@file:OptIn(ExperimentalAtomicApi::class) + package com.vitorpamplona.amethyst.commons.model.observables +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +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.filters.Filter +import kotlin.concurrent.atomics.AtomicReference +import kotlin.concurrent.atomics.ExperimentalAtomicApi /** - * A relay-like list of the notes matching a filter, newest first, that only grows. + * Creates a list of notes (regular and addressable), sorted by created_at like a + * relay, that only grows when a new note appears. * - * The declaration is shared so the event cache can live in `commonMain`; the implementation is - * not. One entry per idHex under concurrent ingest is kept by doing every write to the sorted - * set inside that key's `ConcurrentHashMap.compute` critical section — per-key striping, no - * lock — over a `ConcurrentSkipListSet` whose weakly-consistent iterator the snapshot - * de-duplicates. Kotlin/Native has neither primitive, and the argument for why this is correct - * (spelled out on the jvmAndroid actual) does not survive being reassembled out of weaker ones: - * a copy-on-write `compute` may re-run its lambda, and this lambda has side effects. + * New versions of addressables do not update the list. * - * So iOS has no implementation yet and every member throws — see `NoteListMatchingFilter.ios.kt`. + * There is exactly one [Note] instance per id/address (the cache owns their creation), so + * uniqueness is a non-issue in principle — except a note's sort key is mutable: a newer + * replaceable event swaps the event on the SAME [AddressableNote] instance, changing its + * created_at in place. Anything ordered on that live value cannot survive it. So the sort key is + * snapshotted into an immutable [Entry] when the note first enters and never read live again. + * + * **Lock-free, by copy-on-write.** Observer callbacks fire concurrently from several consume + * threads (relay ingest + UI-side justConsume) and this observer is used everywhere, so it takes + * no lock: the whole list lives in one immutable [State] behind an [AtomicReference], and every + * mutation builds the next state and publishes it with a compare-and-set, retrying if another + * thread won the race. Same shape quartz uses for its native `ConcurrentMap`. + * + * Two things fall out of that, both previously paid for by hand: + * - a reader never sees a torn list, so the emitted snapshot needs no de-duplication pass — + * one entry per idHex holds by construction, since [State.ids] is what gates insertion; + * - the emitted list IS the published state, so emitting costs one map rather than a walk of a + * concurrent structure plus a `HashSet` to strip the duplicates that walk could surface. + * + * The copy is O(n) per write, which is what the previous per-emit snapshot already cost. */ -expect class NoteListMatchingFilter( - filter: Filter, - atOnce: (filter: Filter) -> List, - update: (List) -> Unit, +class NoteListMatchingFilter( + private val filter: Filter, + private val atOnce: (filter: Filter) -> List, + private val update: (List) -> Unit, ) : Observable { + /** A note plus the sort key captured at insertion time, so ordering never depends on mutable state. */ + private class Entry( + val note: Note, + val createdAt: Long, + val id: HexKey, + ) + + /** One immutable version of the list. [ids] is the membership gate, keyed by the stable idHex. */ + private class State( + val entries: List, + val ids: Set, + ) { + fun notes(): List = entries.map { it.note } + + companion object { + val EMPTY = State(emptyList(), emptySet()) + } + } + + // created_at descending, id ascending as a stable tiebreak. Both fields are + // immutable snapshots, so an Entry never moves once inserted. + private val order = + Comparator { a, b -> + val byCreatedAt = b.createdAt.compareTo(a.createdAt) + if (byCreatedAt != 0) byCreatedAt else a.id.compareTo(b.id) + } + + private val state = AtomicReference(State.EMPTY) + + private fun entryFor(note: Note): Entry { + // A null event (unresolved note) sorts last, matching CreatedAtIdHexComparator. + val event = note.event + return Entry(note, note.createdAt() ?: Long.MIN_VALUE, event?.id ?: note.idHex) + } + + private fun State.plus( + entry: Entry, + limit: Int?, + ): State { + val found = entries.binarySearch(entry, order) + val at = if (found < 0) -found - 1 else found + + val grown = ArrayList(entries.size + 1) + grown.addAll(entries.subList(0, at)) + grown.add(entry) + grown.addAll(entries.subList(at, entries.size)) + + if (limit == null || grown.size <= limit) return State(grown, ids + entry.note.idHex) + + // Over the limit: drop the oldest, which sorts last. That can be the entry just + // inserted, and then it is simply not listed — as the previous pollLast() did. + val dropped = grown.removeAt(grown.size - 1) + return State(grown, ids + entry.note.idHex - dropped.note.idHex) + } + + private fun State.minus(idHex: HexKey): State = State(entries.filterNot { it.note.idHex == idHex }, ids - idHex) + override fun new( event: Event, note: Note, - ) + ) { + if (event is AddressableEvent && note !is AddressableNote) return - override fun remove(note: Note) + if (!filter.match(event)) return - fun init() + val limit = filter.limit + while (true) { + val current = state.load() + // Already listed: a newer version of an addressable keeps its entry, and its + // position with it, so the list only ever grows. + if (note.idHex in current.ids) return + + val next = current.plus(entryFor(note), limit) + if (state.compareAndSet(current, next)) { + update(next.notes()) + return + } + } + } + + override fun remove(note: Note) { + while (true) { + val current = state.load() + if (note.idHex !in current.ids) return + + val next = current.minus(note.idHex) + if (state.compareAndSet(current, next)) { + update(next.notes()) + return + } + } + } + + fun init() { + // The cache query behind [atOnce] has already applied the filter's limit, so this + // inserts what it is given, exactly as the previous implementation did. + var fresh = State.EMPTY + atOnce(filter).forEach { note -> + if (note.idHex !in fresh.ids) fresh = fresh.plus(entryFor(note), null) + } + state.store(fresh) + update(fresh.notes()) + } } diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.ios.kt deleted file mode 100644 index 37831da78e..0000000000 --- a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.ios.kt +++ /dev/null @@ -1,52 +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.commons.model.observables - -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter - -actual class EventListMatchingFilter actual constructor( - filter: Filter, - atOnce: (filter: Filter) -> List, - update: (List) -> Unit, -) : Observable { - actual override fun new( - event: Event, - note: Note, - ) { - notYetImplemented() - } - - actual override fun remove(note: Note) { - notYetImplemented() - } - - actual fun init() { - notYetImplemented() - } -} - -private fun notYetImplemented(): Nothing = - throw NotImplementedError( - "The matching-filter observables have no iOS implementation yet: they need a per-key " + - "atomic section over a sorted concurrent set.", - ) diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.ios.kt deleted file mode 100644 index be2bb3e51f..0000000000 --- a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.ios.kt +++ /dev/null @@ -1,52 +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.commons.model.observables - -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter - -actual class NoteListMatchingFilter actual constructor( - filter: Filter, - atOnce: (filter: Filter) -> List, - update: (List) -> Unit, -) : Observable { - actual override fun new( - event: Event, - note: Note, - ) { - notYetImplemented() - } - - actual override fun remove(note: Note) { - notYetImplemented() - } - - actual fun init() { - notYetImplemented() - } -} - -private fun notYetImplemented(): Nothing = - throw NotImplementedError( - "The matching-filter observables have no iOS implementation yet: they need a per-key " + - "atomic section over a sorted concurrent set.", - ) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.jvmAndroid.kt deleted file mode 100644 index 0e5a6ec4cc..0000000000 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.jvmAndroid.kt +++ /dev/null @@ -1,167 +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.commons.model.observables - -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -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.filters.Filter -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentSkipListSet - -/** - * Creates a list of events (regular and addressable), sorted by created_at, that - * is updated every time a new matching event is received — INCLUDING newer - * versions of addressables, whose refreshed content re-emits and re-sorts. - * - * Like [NoteListMatchingFilter], this cannot store mutable [Note]s in a set - * ordered on their live created_at: a newer replaceable event mutates the SAME - * [AddressableNote] instance in place, which strands its node and lets the same - * instance be inserted twice — the emitted list would then carry the same event - * twice. So the sort key is snapshotted into an immutable [Entry], [sorted] is - * ordered on that snapshot, and [byId] is the membership source of truth keyed - * by the stable idHex (the address for addressables, the event id otherwise). - * - * Unlike [NoteListMatchingFilter], an addressable update is NOT ignored: the - * list re-emits so consumers pick up the refreshed [Event] (read live off the - * note). The entry's captured position is kept — re-sorting an updated entry - * would mean a remove+add on [sorted], and two entries with different captured - * keys for the same note would then transiently coexist and both read the same - * live event, duplicating it. Keeping the position matches the original's only - * non-corrupting behavior (it never reliably re-sorted either); consumers that - * care about order re-sort downstream. - * - * Observer callbacks fire concurrently from several consume threads (relay - * ingest + UI-side justConsume), so it stays lock-free: [sorted] is only ever - * written for a key INSIDE that key's `compute` critical section, and only while - * the key is absent. ConcurrentHashMap stripes per key, so same-idHex ops - * serialize while different keys run in parallel. An entry is added only while - * its key is absent from [byId], and every path that frees a key removes its - * entry from [sorted] first. - * - * That keeps [sorted] converged to one entry per idHex, but a - * ConcurrentSkipListSet iterator is only weakly consistent: under concurrent - * add/remove churn a single traversal can momentarily surface a key twice - * (lazy-deleted node not yet unlinked while its replacement is inserted). The - * emitted list must never carry a duplicate id — a LazyColumn keyed on it would - * crash — so [snapshot] deduplicates by the stable idHex as it materializes. - */ -actual class EventListMatchingFilter actual constructor( - private val filter: Filter, - private val atOnce: (filter: Filter) -> List, - private val update: (List) -> Unit, -) : Observable { - /** A note plus the sort key captured at insertion time, so ordering never depends on mutable state. */ - private class Entry( - val note: Note, - val createdAt: Long, - val id: HexKey, - ) - - // created_at descending, id ascending as a stable tiebreak. Both fields are - // immutable snapshots, so an Entry never moves once inserted. - private val order = - Comparator { a, b -> - val byCreatedAt = b.createdAt.compareTo(a.createdAt) - if (byCreatedAt != 0) byCreatedAt else a.id.compareTo(b.id) - } - - private val sorted = ConcurrentSkipListSet(order) - private val byId = ConcurrentHashMap() - - private fun entryFor(note: Note): Entry { - val event = note.event - return Entry(note, note.createdAt() ?: Long.MIN_VALUE, event?.id ?: note.idHex) - } - - @Suppress("UNCHECKED_CAST") - actual override fun new( - event: Event, - note: Note, - ) { - if (event is AddressableEvent && note !is AddressableNote) { - // The "version" note (a regular note holding an addressable event) is - // never stored — the AddressableNote is. Re-emit if that addressable - // is already listed so consumers pick up the refreshed content. - if (byId.containsKey(event.address().toValue())) update(snapshot()) - return - } - - if (!filter.match(event)) return - - // Add to [sorted] atomically with claiming the idHex slot, only when the - // key is absent. An update keeps its entry (and position) — the re-emit - // below reflects the refreshed event read live off the note. - var added = false - byId.compute(note.idHex) { _, existing -> - existing ?: entryFor(note).also { - sorted.add(it) - added = true - } - } - - if (added) { - val limit = filter.limit - if (limit != null && sorted.size > limit) { - // Drop the oldest (sorts last under [order]). - sorted.pollLast()?.let { byId.remove(it.note.idHex, it) } - } - } - - // Always re-emit on a match: a first insert grows the list, an update - // refreshes the event content the snapshot reads off the note. - update(snapshot()) - } - - @Suppress("UNCHECKED_CAST") - actual override fun remove(note: Note) { - var removed = false - byId.compute(note.idHex) { _, existing -> - if (existing != null) { - sorted.remove(existing) - removed = true - } - null - } - if (removed) update(snapshot()) - } - - @Suppress("UNCHECKED_CAST") - actual fun init() { - sorted.clear() - byId.clear() - atOnce(filter).forEach { note -> - byId.computeIfAbsent(note.idHex) { entryFor(note).also { sorted.add(it) } } - } - update(snapshot()) - } - - @Suppress("UNCHECKED_CAST") - private fun snapshot(): List { - // Dedup by the stable idHex: the weakly-consistent iterator can transiently - // surface a key twice under concurrent churn. Both would read the same live - // event off the same note, so keeping the first (newest position) is correct. - val seen = HashSet() - return sorted.mapNotNull { e -> if (seen.add(e.note.idHex)) e.note.event as? T else null } - } -} diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.jvmAndroid.kt deleted file mode 100644 index f3466a548d..0000000000 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.jvmAndroid.kt +++ /dev/null @@ -1,154 +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.commons.model.observables - -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -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.filters.Filter -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentSkipListSet - -/** - * Creates a list of notes (regular and addressable), sorted by created_at like a - * relay, that only grows when a new note appears. - * - * New versions of addressables do not update the list. - * - * There is exactly one [Note] instance per id/address (LocalCache owns their - * creation), so uniqueness is a non-issue in principle — except a note's sort - * key is mutable: a newer replaceable event swaps the event on the SAME - * [AddressableNote] instance, changing its created_at in place. A sorted set - * ordered on that live value cannot survive it — the moved node is no longer on - * the search path of add()/remove(), so the same instance gets inserted twice - * and the emitted list carries a duplicate idHex, crashing any LazyColumn keyed - * on it. - * - * So the sort key is snapshotted into an immutable [Entry] when the note first - * enters and never read live again; [sorted] is ordered on that snapshot - * (stable) and [byId] is the membership source of truth, keyed by the stable - * idHex. - * - * Observer callbacks fire concurrently from several consume threads (relay - * ingest + UI-side justConsume), and this observer is used everywhere, so it - * stays lock-free: [byId] is a ConcurrentHashMap and every write to [sorted] for - * a given idHex happens INSIDE that key's `compute` critical section. - * ConcurrentHashMap stripes per key, so same-idHex ops serialize while different - * keys run fully in parallel. An entry is added to [sorted] only while its key - * is absent from [byId], and every path that makes a key absent removes its - * entry from [sorted] first, keeping [sorted] converged to one entry per idHex. - * - * That convergence isn't enough on its own: a ConcurrentSkipListSet iterator is - * only weakly consistent, so under concurrent add/remove churn a single - * traversal can momentarily surface a key twice (a lazy-deleted node not yet - * unlinked while its replacement is inserted). The emitted list must never carry - * a duplicate idHex — the LazyColumn keyed on it would crash — so [snapshot] - * deduplicates by idHex as it materializes. - */ -actual class NoteListMatchingFilter actual constructor( - private val filter: Filter, - private val atOnce: (filter: Filter) -> List, - private val update: (List) -> Unit, -) : Observable { - /** A note plus the sort key captured at insertion time, so ordering never depends on mutable state. */ - private class Entry( - val note: Note, - val createdAt: Long, - val id: HexKey, - ) - - // created_at descending, id ascending as a stable tiebreak. Both fields are - // immutable snapshots, so an Entry never moves once inserted. - private val order = - Comparator { a, b -> - val byCreatedAt = b.createdAt.compareTo(a.createdAt) - if (byCreatedAt != 0) byCreatedAt else a.id.compareTo(b.id) - } - - private val sorted = ConcurrentSkipListSet(order) - private val byId = ConcurrentHashMap() - - private fun entryFor(note: Note): Entry { - // A null event (unresolved note) sorts last, matching CreatedAtIdHexComparator. - val event = note.event - return Entry(note, note.createdAt() ?: Long.MIN_VALUE, event?.id ?: note.idHex) - } - - actual override fun new( - event: Event, - note: Note, - ) { - if (event is AddressableEvent && note !is AddressableNote) return - - if (!filter.match(event)) return - - // Add to [sorted] atomically with claiming the idHex slot. New versions - // of an already listed note return the existing entry untouched. - var added = false - byId.compute(note.idHex) { _, existing -> - existing ?: entryFor(note).also { - sorted.add(it) - added = true - } - } - if (!added) return - - val limit = filter.limit - if (limit != null && sorted.size > limit) { - // Drop the oldest (sorts last under [order]). - sorted.pollLast()?.let { byId.remove(it.note.idHex, it) } - } - - update(snapshot()) - } - - actual override fun remove(note: Note) { - // Remove from [sorted] atomically with releasing the idHex slot. - var removed = false - byId.compute(note.idHex) { _, existing -> - if (existing != null) { - sorted.remove(existing) - removed = true - } - null - } - if (removed) update(snapshot()) - } - - actual fun init() { - sorted.clear() - byId.clear() - atOnce(filter).forEach { note -> - byId.computeIfAbsent(note.idHex) { entryFor(note).also { sorted.add(it) } } - } - update(snapshot()) - } - - private fun snapshot(): List { - // Dedup by idHex: the weakly-consistent iterator can transiently surface a - // key twice under concurrent churn. Keeping the first (newest position) is - // correct — both nodes point at the same note. - val seen = HashSet() - return sorted.mapNotNull { e -> e.note.takeIf { seen.add(it.idHex) } } - } -} From 59e79a04be8ce99a00de6440649584ace97d238c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 18:26:02 +0000 Subject: [PATCH 22/29] test(desktop): pin the cache fixtures against the weak note store `DesktopCachePipelineTest` consumes events and then queries the cache for the notes they produced. Nothing in between held a strong reference to those notes -- until this branch, `DesktopLocalCache.notesByAuthor` did, incidentally: an unbounded `ConcurrentHashMap>` that pinned every note the Desktop app ever saw and quietly defeated the point of storing them in a `LargeSoftCache`. Removing that index was the right call, but it took the tests' only referent with it, so a GC landing between `consume` and the assertion now empties the cache. That is what turned `compose-ui-test` red: `FollowingFeedFilter only includes notes from followed users` got `expected:<1> but was:<0>`. It reproduces exactly with two `System.gc()` calls before the query, and it is not specific to that test -- all 46 consume sites have the same shape, so CI's heap picked the victim. Route every consume through an `ingest` helper that pins what the cache built for the lifetime of the test instance, the way a screen holds the notes it is showing in the app. Keep the forced GC in the test that failed, so the contract is asserted rather than left to the heap. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../desktop/cache/DesktopCachePipelineTest.kt | 123 +++++++++++------- 1 file changed, 77 insertions(+), 46 deletions(-) diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt index 0f1ba2911f..8cb6212cc1 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.desktop.feeds.DesktopNotificationFeedFilter import com.vitorpamplona.amethyst.desktop.feeds.DesktopProfileFeedFilter import com.vitorpamplona.amethyst.desktop.feeds.DesktopThreadFilter import com.vitorpamplona.amethyst.desktop.viewmodels.DesktopFeedViewModel +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent @@ -65,6 +66,28 @@ class DesktopCachePipelineTest { /** Wait for async bundling (250ms bundler + margin) */ private suspend fun waitForBundler() = delay(500) + /** + * Strong references to everything the cache built during one test. + * + * `DesktopLocalCache.notes`, `users` and `addressableNotes` are LargeSoftCaches: + * their values are held weakly, so a note nothing else points at can be collected + * between the `consume` that created it and the assertion that looks for it. In the + * app the screen showing a note is that strong referent; a test has none, so + * [ingest] pins the fixtures here for the lifetime of the test instance. Without + * this, every consume-then-query test below fails non-deterministically whenever a + * GC lands in that window -- which is what CI's tighter heap produces. + */ + private val pinnedFixtures = mutableListOf() + + /** Consumes [event] into the cache and pins whatever it created. See [pinnedFixtures]. */ + private fun DesktopLocalCache.ingest(event: Event): Boolean { + val consumed = consume(event, relayUrl, wasVerified = true) + pinnedFixtures.addAll(users.filterIntoSet { _, _ -> true }) + pinnedFixtures.addAll(notes.filterIntoSet { _, _ -> true }) + pinnedFixtures.addAll(addressableNotes.filterIntoSet { _, _ -> true }) + return consumed + } + private fun textNote( id: String, pubKey: String, @@ -127,7 +150,7 @@ class DesktopCachePipelineTest { val cache = DesktopLocalCache().apply { accountPubkey = userPubKey } val event = textNote("note1".padEnd(64, '0'), userPubKey) - val consumed = cache.consume(event, relayUrl, wasVerified = true) + val consumed = cache.ingest(event) assertTrue(consumed, "First consume should return true") val note = cache.getNoteIfExists("note1".padEnd(64, '0')) @@ -140,8 +163,8 @@ class DesktopCachePipelineTest { val cache = DesktopLocalCache().apply { accountPubkey = userPubKey } val event = textNote("note1".padEnd(64, '0'), userPubKey) - cache.consume(event, relayUrl, wasVerified = true) - val secondConsume = cache.consume(event, relayUrl, wasVerified = true) + cache.ingest(event) + val secondConsume = cache.ingest(event) assertTrue(!secondConsume, "Second consume of same event should return false") } @@ -151,7 +174,7 @@ class DesktopCachePipelineTest { val cache = DesktopLocalCache().apply { accountPubkey = userPubKey } val event = contactList("cl1".padEnd(64, '0'), userPubKey, listOf(followedPubKey)) - cache.consume(event, relayUrl, wasVerified = true) + cache.ingest(event) assertEquals(setOf(followedPubKey), cache.followedUsers.value) } @@ -168,8 +191,8 @@ class DesktopCachePipelineTest { createdAt = 200, ) - cache.consume(old, relayUrl, wasVerified = true) - cache.consume(newer, relayUrl, wasVerified = true) + cache.ingest(old) + cache.ingest(newer) assertEquals(setOf(followedPubKey, unfollowedPubKey), cache.followedUsers.value) } @@ -180,8 +203,8 @@ class DesktopCachePipelineTest { val newer = contactList("cl2".padEnd(64, '0'), userPubKey, listOf(followedPubKey, unfollowedPubKey), createdAt = 200) val old = contactList("cl1".padEnd(64, '0'), userPubKey, listOf(followedPubKey), createdAt = 100) - cache.consume(newer, relayUrl, wasVerified = true) - cache.consume(old, relayUrl, wasVerified = true) + cache.ingest(newer) + cache.ingest(old) assertEquals( setOf(followedPubKey, unfollowedPubKey), @@ -197,8 +220,8 @@ class DesktopCachePipelineTest { val note = textNote(noteId, userPubKey) val react = reaction("react1".padEnd(64, '0'), followedPubKey, noteId) - cache.consume(note, relayUrl, wasVerified = true) - cache.consume(react, relayUrl, wasVerified = true) + cache.ingest(note) + cache.ingest(react) val cachedNote = cache.getNoteIfExists(noteId)!! assertTrue(cachedNote.countReactions() > 0, "Note should have reactions after consuming reaction event") @@ -223,7 +246,7 @@ class DesktopCachePipelineTest { val event = contactList("cl1".padEnd(64, '0'), userPubKey, listOf(followedPubKey), createdAt = 100) // Phase A — hydration path: consume with accountPubkey unbound. - cache.consume(event, relayUrl, wasVerified = true) + cache.ingest(event) assertEquals( emptySet(), cache.followedUsers.value, @@ -235,7 +258,7 @@ class DesktopCachePipelineTest { // Phase C — relay replay of the SAME event. Must NOT be rejected by // the createdAt gate; must populate _followedUsers. - cache.consume(event, relayUrl, wasVerified = true) + cache.ingest(event) assertEquals( setOf(followedPubKey), cache.followedUsers.value, @@ -248,7 +271,7 @@ class DesktopCachePipelineTest { val cache = DesktopLocalCache() val other = contactList("cl2".padEnd(64, '0'), followedPubKey, listOf(unfollowedPubKey), createdAt = 100) - cache.consume(other, relayUrl, wasVerified = true) + cache.ingest(other) cache.accountPubkey = userPubKey // followedUsers is for the active user only; a non-self kind-3 @@ -260,7 +283,7 @@ class DesktopCachePipelineTest { // reject; here we skipped stamping when pubkey was null so a // newer replay lands cleanly). val newer = contactList("cl2b".padEnd(64, '0'), followedPubKey, listOf(unfollowedPubKey, userPubKey), createdAt = 200) - cache.consume(newer, relayUrl, wasVerified = true) + cache.ingest(newer) // No direct assertion on internal state; the fact that this // returns without throwing + does not affect followedUsers is // the invariant. The next line documents intent. @@ -286,7 +309,7 @@ class DesktopCachePipelineTest { delay(50) val event = textNote("note1".padEnd(64, '0'), userPubKey) - cache.consume(event, relayUrl, wasVerified = true) + cache.ingest(event) val note = cache.getNoteIfExists(event.id)!! cache.emitNewNotes(setOf(note)) @@ -307,9 +330,9 @@ class DesktopCachePipelineTest { val filter = DesktopGlobalFeedFilter(cache) // Add notes from different authors - cache.consume(textNote("n1".padEnd(64, '0'), userPubKey, createdAt = 100), relayUrl, wasVerified = true) - cache.consume(textNote("n2".padEnd(64, '0'), followedPubKey, createdAt = 200), relayUrl, wasVerified = true) - cache.consume(textNote("n3".padEnd(64, '0'), unfollowedPubKey, createdAt = 300), relayUrl, wasVerified = true) + cache.ingest(textNote("n1".padEnd(64, '0'), userPubKey, createdAt = 100)) + cache.ingest(textNote("n2".padEnd(64, '0'), followedPubKey, createdAt = 200)) + cache.ingest(textNote("n3".padEnd(64, '0'), unfollowedPubKey, createdAt = 300)) val feed = filter.feed() assertEquals(3, feed.size, "Global feed should contain all text notes") @@ -318,12 +341,20 @@ class DesktopCachePipelineTest { @Test fun `FollowingFeedFilter only includes notes from followed users`() { val cache = DesktopLocalCache().apply { accountPubkey = userPubKey } - cache.consume(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey)), relayUrl, wasVerified = true) + cache.ingest(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey))) - cache.consume(textNote("n1".padEnd(64, '0'), followedPubKey, createdAt = 100), relayUrl, wasVerified = true) - cache.consume(textNote("n2".padEnd(64, '0'), unfollowedPubKey, createdAt = 200), relayUrl, wasVerified = true) + cache.ingest(textNote("n1".padEnd(64, '0'), followedPubKey, createdAt = 100)) + cache.ingest(textNote("n2".padEnd(64, '0'), unfollowedPubKey, createdAt = 200)) val filter = DesktopFollowingFeedFilter(cache) { cache.followedUsers.value } + + // Deliberate: force the weak cache's hand before querying it, so the fixtures' + // survival is asserted here instead of left to whatever the heap happens to do. + // Without [ingest]'s pinning this collects the notes and the feed comes back empty + // -- the shape that failed on CI and passed on a roomier dev heap. + System.gc() + System.gc() + val feed = filter.feed() assertEquals(1, feed.size, "Following feed should only contain notes from followed users") @@ -333,7 +364,7 @@ class DesktopCachePipelineTest { @Test fun `FollowingFeedFilter returns empty when no follows`() { val cache = DesktopLocalCache().apply { accountPubkey = userPubKey } - cache.consume(textNote("n1".padEnd(64, '0'), followedPubKey), relayUrl, wasVerified = true) + cache.ingest(textNote("n1".padEnd(64, '0'), followedPubKey)) val filter = DesktopFollowingFeedFilter(cache) { emptySet() } val feed = filter.feed() @@ -344,8 +375,8 @@ class DesktopCachePipelineTest { @Test fun `ProfileFeedFilter only shows notes from target pubkey`() { val cache = DesktopLocalCache().apply { accountPubkey = userPubKey } - cache.consume(textNote("n1".padEnd(64, '0'), followedPubKey, createdAt = 100), relayUrl, wasVerified = true) - cache.consume(textNote("n2".padEnd(64, '0'), unfollowedPubKey, createdAt = 200), relayUrl, wasVerified = true) + cache.ingest(textNote("n1".padEnd(64, '0'), followedPubKey, createdAt = 100)) + cache.ingest(textNote("n2".padEnd(64, '0'), unfollowedPubKey, createdAt = 200)) val filter = DesktopProfileFeedFilter(followedPubKey, cache) val feed = filter.feed() @@ -360,8 +391,8 @@ class DesktopCachePipelineTest { val rootId = "root".padEnd(64, '0') val replyId = "reply".padEnd(64, '0') - cache.consume(textNote(rootId, userPubKey, createdAt = 100), relayUrl, wasVerified = true) - cache.consume(textNote(replyId, followedPubKey, createdAt = 200, replyToId = rootId), relayUrl, wasVerified = true) + cache.ingest(textNote(rootId, userPubKey, createdAt = 100)) + cache.ingest(textNote(replyId, followedPubKey, createdAt = 200, replyToId = rootId)) val filter = DesktopThreadFilter(rootId, cache) val feed = filter.feed() @@ -373,11 +404,11 @@ class DesktopCachePipelineTest { fun `NotificationFeedFilter shows events tagging user`() { val cache = DesktopLocalCache().apply { accountPubkey = userPubKey } val noteId = "note1".padEnd(64, '0') - cache.consume(textNote(noteId, userPubKey, createdAt = 100), relayUrl, wasVerified = true) + cache.ingest(textNote(noteId, userPubKey, createdAt = 100)) // Reaction from someone else targeting user's note val react = reaction("react1".padEnd(64, '0'), followedPubKey, noteId, createdAt = 200) - cache.consume(react, relayUrl, wasVerified = true) + cache.ingest(react) val filter = DesktopNotificationFeedFilter(userPubKey, cache) val feed = filter.feed() @@ -397,7 +428,7 @@ class DesktopCachePipelineTest { fun `ViewModel starts in Loading then transitions to Loaded after refresh`() = runBlocking { val cache = DesktopLocalCache().apply { accountPubkey = userPubKey } - cache.consume(textNote("n1".padEnd(64, '0'), userPubKey), relayUrl, wasVerified = true) + cache.ingest(textNote("n1".padEnd(64, '0'), userPubKey)) val vm = DesktopFeedViewModel(DesktopGlobalFeedFilter(cache), cache) @@ -436,7 +467,7 @@ class DesktopCachePipelineTest { // Simulate relay event arriving val event = textNote("n1".padEnd(64, '0'), userPubKey) - cache.consume(event, relayUrl, wasVerified = true) + cache.ingest(event) val note = cache.getNoteIfExists(event.id)!! cache.emitNewNotes(setOf(note)) @@ -452,7 +483,7 @@ class DesktopCachePipelineTest { fun `Following ViewModel only shows followed users notes via eventStream`() = runBlocking { val cache = DesktopLocalCache().apply { accountPubkey = userPubKey } - cache.consume(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey)), relayUrl, wasVerified = true) + cache.ingest(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey))) val filter = DesktopFollowingFeedFilter(cache) { cache.followedUsers.value } val vm = DesktopFeedViewModel(filter, cache) @@ -460,7 +491,7 @@ class DesktopCachePipelineTest { // Add followed user's note val e1 = textNote("n1".padEnd(64, '0'), followedPubKey, createdAt = 100) - cache.consume(e1, relayUrl, wasVerified = true) + cache.ingest(e1) val note1 = cache.getNoteIfExists(e1.id)!! cache.emitNewNotes(setOf(note1)) waitForBundler() @@ -469,7 +500,7 @@ class DesktopCachePipelineTest { // Add unfollowed user's note val e2 = textNote("n2".padEnd(64, '0'), unfollowedPubKey, createdAt = 200) - cache.consume(e2, relayUrl, wasVerified = true) + cache.ingest(e2) val note2 = cache.getNoteIfExists(e2.id)!! cache.emitNewNotes(setOf(note2)) waitForBundler() @@ -485,7 +516,7 @@ class DesktopCachePipelineTest { // No contact list consumed — followedUsers remains empty val e1 = textNote("n1".padEnd(64, '0'), followedPubKey) - cache.consume(e1, relayUrl, wasVerified = true) + cache.ingest(e1) val filter = DesktopFollowingFeedFilter(cache) { cache.followedUsers.value } val vm = DesktopFeedViewModel(filter, cache) @@ -505,8 +536,8 @@ class DesktopCachePipelineTest { @Test fun `clear resets all cache state`() { val cache = DesktopLocalCache().apply { accountPubkey = userPubKey } - cache.consume(textNote("n1".padEnd(64, '0'), userPubKey), relayUrl, wasVerified = true) - cache.consume(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey)), relayUrl, wasVerified = true) + cache.ingest(textNote("n1".padEnd(64, '0'), userPubKey)) + cache.ingest(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey))) cache.clear() @@ -522,9 +553,9 @@ class DesktopCachePipelineTest { @Test fun `global feed is sorted newest first`() { val cache = DesktopLocalCache().apply { accountPubkey = userPubKey } - cache.consume(textNote("old".padEnd(64, '0'), userPubKey, createdAt = 100), relayUrl, wasVerified = true) - cache.consume(textNote("mid".padEnd(64, '0'), userPubKey, createdAt = 200), relayUrl, wasVerified = true) - cache.consume(textNote("new".padEnd(64, '0'), userPubKey, createdAt = 300), relayUrl, wasVerified = true) + cache.ingest(textNote("old".padEnd(64, '0'), userPubKey, createdAt = 100)) + cache.ingest(textNote("mid".padEnd(64, '0'), userPubKey, createdAt = 200)) + cache.ingest(textNote("new".padEnd(64, '0'), userPubKey, createdAt = 300)) val filter = DesktopGlobalFeedFilter(cache) val feed = filter.feed() @@ -550,7 +581,7 @@ class DesktopCachePipelineTest { sig = dummySig, ) - cache.consume(metadata, relayUrl, wasVerified = true) + cache.ingest(metadata) val user = cache.getUserIfExists(userPubKey) assertTrue(user != null, "User should exist after metadata consumption") @@ -569,12 +600,12 @@ class DesktopCachePipelineTest { // Create a text note val textEvent = textNote("t1".padEnd(64, '0'), userPubKey) - cache.consume(textEvent, relayUrl, wasVerified = true) + cache.ingest(textEvent) val textNote = cache.getNoteIfExists(textEvent.id)!! // Create a reaction (not a text note) val reactEvent = reaction("r1".padEnd(64, '0'), userPubKey, "t1".padEnd(64, '0')) - cache.consume(reactEvent, relayUrl, wasVerified = true) + cache.ingest(reactEvent) val reactNote = cache.getNoteIfExists(reactEvent.id)!! val filtered = filter.applyFilter(setOf(textNote, reactNote)) @@ -586,16 +617,16 @@ class DesktopCachePipelineTest { @Test fun `FollowingFeedFilter applyFilter respects follow set`() { val cache = DesktopLocalCache().apply { accountPubkey = userPubKey } - cache.consume(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey)), relayUrl, wasVerified = true) + cache.ingest(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey))) val filter = DesktopFollowingFeedFilter(cache) { cache.followedUsers.value } val e1 = textNote("n1".padEnd(64, '0'), followedPubKey) - cache.consume(e1, relayUrl, wasVerified = true) + cache.ingest(e1) val note1 = cache.getNoteIfExists(e1.id)!! val e2 = textNote("n2".padEnd(64, '0'), unfollowedPubKey) - cache.consume(e2, relayUrl, wasVerified = true) + cache.ingest(e2) val note2 = cache.getNoteIfExists(e2.id)!! val filtered = filter.applyFilter(setOf(note1, note2)) @@ -655,7 +686,7 @@ class DesktopCachePipelineTest { sig = dummySig, ) - cache.consume(metadata, relayUrl, wasVerified = true) + cache.ingest(metadata) val user = cache.getUserIfExists(userPubKey)!! val cached = user.metadataOrNull() From b79217b09c77d559cde144a8df4f4f4226fadb05 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 18:42:13 +0000 Subject: [PATCH 23/29] perf(commons): halve the list observables' write cost, and measure it The copy-on-write rewrite of the two list observables was justified on portability -- `java.util.concurrent` has no KMP equivalent, which is what kept them in `jvmAndroid` behind an iOS stub. That says nothing about speed, and these are hot: every consumed event is offered to each observer whose filter could match it, from each relay's socket coroutine. So measure it. `ObserverListBenchmark` keeps the `ConcurrentSkipListSet` implementation verbatim as the baseline and as a differential oracle -- `bothImplementationsAgree` asserts the two emit identical lists for identical input at limits null/50/400, which is the property the rewrite had to preserve. Runs in ~7s, asserts on correctness only, never on wall time. The first run said the rewrite was a regression: slower on the limited insert path and 4.9x slower on 8 threads, with no thread scaling. The cause was one line, not the design. `State.plus` published `ids + added`, and over the limit `ids + added - dropped`; each operator allocates a full copy of the set, so the eviction path rebuilt membership twice per insert. One `HashSet`, sized up front and mutated before publishing, is the fix. After it, against the implementation it replaced: re-delivering an already listed note 3.3-4.1x faster, insert into a populated unlimited list (what nearly every observer registers) 1.3-1.5x faster and widening with n, cold fill at n=5000 1.8x faster, concurrent inserts at parity on one thread and 2.1-2.7x slower on 4-8. That last number is a real cost and is written on both classes rather than left out: threads serialize on one reference and a lost CAS discards its copy, where the skip list striped across keys and scaled. Two things bound it -- the benchmark's threads do nothing but insert, while real ingest spends most of its per-event budget verifying signatures and parsing before it reaches an observer; and nearly every production observer registers with no limit, which is the shape copy-on-write wins. Also recorded, since it framed the earlier discussion wrong: lock-free was never the difference between the two. The skip-list version was lock-free too -- ConcurrentHashMap stripes per key, so `compute` holds a bin, not a monitor. The choice was portability and speed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../2026-08-30-commons-migration-sweep.md | 83 ++++ .../observables/EventListMatchingFilter.kt | 28 +- .../observables/NoteListMatchingFilter.kt | 28 +- .../prodbench/ObserverListBenchmark.kt | 377 ++++++++++++++++++ 4 files changed, 512 insertions(+), 4 deletions(-) create mode 100644 commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/prodbench/ObserverListBenchmark.kt diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index b3dd5e48fa..20982ddae2 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1219,3 +1219,86 @@ Verified: `:commons:compileCommonMainKotlinMetadata`, `:commons:jvmTest`, `:desktopApp:test`, `:cli:test`, `:amethyst:compileFdroidDebugKotlin`, `:amethyst:testPlayDebugUnitTest`, `spotlessCheck`. + +## Step 8 — measuring the copy-on-write observables (the "is it actually faster?" question) + +The rewrite of `NoteListMatchingFilter` / `EventListMatchingFilter` from +`ConcurrentSkipListSet` + `ConcurrentHashMap` to copy-on-write over an +`AtomicReference` was made for **portability** — `java.util.concurrent` has no +KMP equivalent, and that is what kept these two in `jvmAndroid` with an iOS +stub. That argument says nothing about speed, and these sit on a hot path: +every consumed event is offered to each observer whose filter could match it, +from each relay's socket coroutine. A few hundred events a second across a +dozen relays reaches this code thousands of times a second. + +So it was measured rather than argued. `ObserverListBenchmark` +(`commons/src/jvmTest/.../prodbench/`) keeps the skip-list implementation +verbatim as the baseline and as a **differential oracle**: `bothImplementations +Agree` asserts the two emit identical lists for identical input at limits +`null` / 50 / 400, which is the property the rewrite had to preserve. It runs +in ~7s and asserts only on correctness, never on wall time. + +**What the first run found:** the copy-on-write version was *slower* on the +limited-filter insert path and **4.9x slower on 8 concurrent threads**, with +zero thread scaling. The cause was not the design but one line — +`State.plus` published `ids + added` and, over the limit, `ids + added - +dropped`. Each operator allocates a full copy of the set, so the eviction path +rebuilt the membership set **twice per insert**. Replacing both with a single +`HashSet` copy (sized up front, mutated, then published) is the whole of the +fix. + +**After that fix**, against the implementation it replaced: + +| shape | result | +|---|---| +| re-deliver an already listed note (steady state once a screen is warm) | **3.3–4.1x faster** | +| insert into a populated *unlimited* list, n = 100 / 1000 | **1.3–1.5x faster**, widening with n | +| cold fill, n = 5000 | **1.8x faster** | +| concurrent inserts into the same observer, 1 thread | parity | +| concurrent inserts into the same observer, 4 / 8 threads | **2.1–2.7x slower** | + +The last row is the real cost and is documented on both classes rather than +buried here: threads serialize on one reference and a lost CAS discards its +copy, where the skip list striped across keys and scaled with thread count. Two +things bound it. The benchmark's threads do nothing but insert, while real +ingest spends most of its per-event budget on signature verification and +parsing before reaching an observer, so the contention window is a fraction of +the measured one. And nearly every production observer registers with **no +`limit`** — grep the `observeNotes` / `observeEvents` call sites — which is the +shape copy-on-write wins. + +Worth revisiting if a profile ever disagrees. The lever would be the emit, not +the lock: both implementations already materialize the whole list on every +write, so an observer that only ever appends is paying O(n) to tell the UI +about one new row. + +**Also worth recording:** "lock-free" was never the differentiator between the +two. The skip-list version was lock-free too — `ConcurrentHashMap` stripes per +key, so `compute` holds one bin, not a monitor. The choice was portability and +speed, not locking. + +Verified: `:commons:jvmTest`, `:commons:compileIosMainKotlinMetadata`, +`:commons:spotlessApply`. + +## Step 9 — the weak note cache has no strong referent in tests + +`compose-ui-test` went red on `DesktopCachePipelineTest`: +`FollowingFeedFilter only includes notes from followed users`, `expected:<1> but +was:<0>`, and it would not reproduce on a dev machine. + +The cause is this branch, indirectly. `DesktopLocalCache` kept a +`notesByAuthor: ConcurrentHashMap>` index for metadata +invalidation — an unbounded strong map holding every note the Desktop app ever +saw, which quietly defeated the point of storing them in a `LargeSoftCache`. +Removing it was right (the Android cache never had one). But it was also the +only thing keeping the test fixtures alive: every test consumes events and then +queries the cache for the notes they produced, with nothing in between holding a +reference. A GC landing in that window empties the cache. + +It reproduces deterministically with two `System.gc()` calls before the query, +and it is not specific to that one test — all 46 consume sites have the shape, +so CI's tighter heap just picked the victim. The fix routes every consume +through an `ingest` helper that pins what the cache built for the lifetime of +the test instance, the way a screen holds the notes it is showing in the app, +and keeps the forced GC in the test that failed so the contract is asserted +rather than left to the heap. diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt index dba6c0eb20..303723c9fa 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt @@ -56,6 +56,21 @@ import kotlin.concurrent.atomics.ExperimentalAtomicApi * concurrent structure plus a `HashSet` to strip the duplicates that walk could surface. * * The copy is O(n) per write, which is what the previous per-emit snapshot already cost. + * + * **Measured**, against the `ConcurrentSkipListSet` implementation this replaced — see + * `ObserverListBenchmark`, which keeps that implementation as the baseline and as a + * differential oracle: + * - re-delivery of an already listed note (the steady state once a screen is warm): 3.3–4.1x + * faster, because it is a load plus a set lookup where the skip list took a `compute`; + * - insert into a populated unlimited list (what nearly every observer registers): 1.3–1.5x + * faster at n = 100–1000, widening with n; + * - concurrent inserts into the SAME observer: at parity on one thread, 2.1–2.7x slower on + * 4–8. That is the honest cost of this design: threads serialize on one reference and a + * lost CAS throws away its copy, where the skip list striped across keys and scaled. It is + * measured with threads doing nothing but inserting; real ingest spends most of its + * per-event budget verifying signatures and parsing before it reaches an observer, so the + * contention window is a fraction of that. Worth revisiting if a profile ever says + * otherwise. */ class EventListMatchingFilter( private val filter: Filter, @@ -106,12 +121,21 @@ class EventListMatchingFilter( grown.add(entry) grown.addAll(entries.subList(at, entries.size)) - if (limit == null || grown.size <= limit) return State(grown, ids + entry.note.idHex) + // One copy of the membership set per write. `ids + added` and `ids + added - dropped` + // read well but allocate one full set per operator, so the limit path paid for two; + // measured, that was the whole of this implementation's deficit against the skip list + // on a limited filter. + val nextIds = HashSet(((entries.size + 2) / 0.75f).toInt() + 1) + nextIds.addAll(ids) + nextIds.add(entry.note.idHex) + + if (limit == null || grown.size <= limit) return State(grown, nextIds) // Over the limit: drop the oldest, which sorts last. That can be the entry just // inserted, and then it is simply not listed — as the previous pollLast() did. val dropped = grown.removeAt(grown.size - 1) - return State(grown, ids + entry.note.idHex - dropped.note.idHex) + nextIds.remove(dropped.note.idHex) + return State(grown, nextIds) } private fun State.minus(idHex: HexKey): State = State(entries.filterNot { it.note.idHex == idHex }, ids - idHex) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt index bd8603729b..136cff03cb 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt @@ -56,6 +56,21 @@ import kotlin.concurrent.atomics.ExperimentalAtomicApi * concurrent structure plus a `HashSet` to strip the duplicates that walk could surface. * * The copy is O(n) per write, which is what the previous per-emit snapshot already cost. + * + * **Measured**, against the `ConcurrentSkipListSet` implementation this replaced — see + * `ObserverListBenchmark`, which keeps that implementation as the baseline and as a + * differential oracle: + * - re-delivery of an already listed note (the steady state once a screen is warm): 3.3–4.1x + * faster, because it is a load plus a set lookup where the skip list took a `compute`; + * - insert into a populated unlimited list (what nearly every observer registers): 1.3–1.5x + * faster at n = 100–1000, widening with n; + * - concurrent inserts into the SAME observer: at parity on one thread, 2.1–2.7x slower on + * 4–8. That is the honest cost of this design: threads serialize on one reference and a + * lost CAS throws away its copy, where the skip list striped across keys and scaled. It is + * measured with threads doing nothing but inserting; real ingest spends most of its + * per-event budget verifying signatures and parsing before it reaches an observer, so the + * contention window is a fraction of that. Worth revisiting if a profile ever says + * otherwise. */ class NoteListMatchingFilter( private val filter: Filter, @@ -109,12 +124,21 @@ class NoteListMatchingFilter( grown.add(entry) grown.addAll(entries.subList(at, entries.size)) - if (limit == null || grown.size <= limit) return State(grown, ids + entry.note.idHex) + // One copy of the membership set per write. `ids + added` and `ids + added - dropped` + // read well but allocate one full set per operator, so the limit path paid for two; + // measured, that was the whole of this implementation's deficit against the skip list + // on a limited filter. + val nextIds = HashSet(((entries.size + 2) / 0.75f).toInt() + 1) + nextIds.addAll(ids) + nextIds.add(entry.note.idHex) + + if (limit == null || grown.size <= limit) return State(grown, nextIds) // Over the limit: drop the oldest, which sorts last. That can be the entry just // inserted, and then it is simply not listed — as the previous pollLast() did. val dropped = grown.removeAt(grown.size - 1) - return State(grown, ids + entry.note.idHex - dropped.note.idHex) + nextIds.remove(dropped.note.idHex) + return State(grown, nextIds) } private fun State.minus(idHex: HexKey): State = State(entries.filterNot { it.note.idHex == idHex }, ids - idHex) diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/prodbench/ObserverListBenchmark.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/prodbench/ObserverListBenchmark.kt new file mode 100644 index 0000000000..bccf247fba --- /dev/null +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/prodbench/ObserverListBenchmark.kt @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.prodbench + +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.observables.NoteListMatchingFilter +import com.vitorpamplona.amethyst.commons.model.observables.Observable +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.filters.Filter +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.utils.EventFactory +import java.util.Random +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentSkipListSet +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicLong +import kotlin.concurrent.thread +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Measures [NoteListMatchingFilter], the list every `observeNotes` / `observeEvents` screen + * sits behind, against the `ConcurrentSkipListSet` implementation it replaced. + * + * It is on a hot path: every consumed event is offered to each observer whose filter could + * match it, from each relay's socket coroutine, so a few hundred events a second across a + * dozen relays reach this code thousands of times a second. The rewrite to copy-on-write was + * made to get the class into `commonMain` (`java.util.concurrent` has no KMP equivalent); + * that is a portability argument, and this file is the performance one, which is a separate + * question and has to be answered with numbers rather than asymptotics. + * + * [SkipListReference] is the previous implementation, kept verbatim as the baseline. It also + * serves as a differential oracle: [bothImplementationsAgree] asserts the two produce identical + * output for identical input, which is the property the rewrite actually had to preserve. + * + * Deterministic and offline. Prints ns/op; asserts only on correctness, never on wall time, + * since CI machines vary. + */ +class ObserverListBenchmark { + private val author = "d0d0a746b44c9de8422165aef520b1fe041eedf5794f7592505477eeac122c18" + + /** Touches the emitted list so neither implementation's O(n) materialization is dead code. */ + private val blackhole = AtomicLong() + + private fun sink(list: List) { + blackhole.addAndGet(list.size.toLong() + if (list.isEmpty()) 0 else list[0].idHex.length.toLong()) + } + + private fun event( + i: Int, + createdAt: Long, + ): Event = + EventFactory.create( + id = "%064x".format(i), + pubKey = author, + createdAt = createdAt, + kind = TextNoteEvent.KIND, + tags = emptyArray(), + content = "n", + sig = "00".repeat(64), + ) + + /** + * createdAt is deliberately NOT monotonic in i: a real feed arrives out of order, so the + * insert lands mid-list rather than always at the head, which is the cheapest case for + * both implementations and would flatter whichever one is worse. + */ + private fun fixtures(n: Int): List> { + val rnd = Random(42) + return (0 until n).map { i -> + val e = event(i, 1_700_000_000L + rnd.nextInt(n * 4)) + val note = Note(e.id) + note.event = e + e to note + } + } + + private fun cow(f: Filter) = NoteListMatchingFilter(f, { emptyList() }, ::sink) + + private fun skipList(f: Filter) = SkipListReference(f, ::sink) + + // ---------------------------------------------------------------------- harness + + private fun bench( + label: String, + reps: Int = 5, + warmups: Int = 2, + body: () -> Int, + ): Double { + repeat(warmups) { body() } + val times = ArrayList(reps) + repeat(reps) { + val start = System.nanoTime() + val ops = body() + times.add((System.nanoTime() - start).toDouble() / ops) + } + times.sort() + val median = times[times.size / 2] + println(" %-44s %9.0f ns/op %11.0f ops/s".format(label, median, 1_000_000_000.0 / median)) + return median + } + + private fun compare( + name: String, + cowNs: Double, + skipNs: Double, + ) { + val ratio = cowNs / skipNs + val verdict = + when { + ratio < 0.95 -> "copy-on-write faster x%.2f".format(1 / ratio) + ratio > 1.05 -> "copy-on-write slower x%.2f".format(ratio) + else -> "parity" + } + println(" -> $name: $verdict\n") + } + + // ------------------------------------------------------------------ correctness + + @Test + fun bothImplementationsAgree() { + val fx = fixtures(500) + for (limit in listOf(null, 50, 400)) { + val f = Filter(kinds = listOf(TextNoteEvent.KIND), limit = limit) + + var fromCow: List = emptyList() + val a = NoteListMatchingFilter(f, { emptyList() }, { fromCow = it }) + var fromSkipList: List = emptyList() + val b = SkipListReference(f) { fromSkipList = it } + + fx.forEach { (e, note) -> a.new(e, note) } + fx.forEach { (e, note) -> b.new(e, note) } + + assertEquals( + fromSkipList.map { it.idHex }, + fromCow.map { it.idHex }, + "copy-on-write must emit exactly what the skip list emitted (limit=$limit)", + ) + assertEquals(fromCow.size, fromCow.map { it.idHex }.toSet().size, "no duplicate keys") + + // Re-delivering an already listed note must be a no-op in both. + fx.take(50).forEach { (e, note) -> a.new(e, note) } + fx.take(50).forEach { (e, note) -> b.new(e, note) } + assertEquals(fromSkipList.map { it.idHex }, fromCow.map { it.idHex }, "re-delivery must not change either list") + } + } + + // ------------------------------------------------------------------- serial load + + @Test + fun insertIntoAPopulatedList() { + // No limit: that is what nearly every production observer passes, so the list grows + // to whatever the cache holds for that kind. + println("\n== insert into a list already holding n (seed excluded from timing, no limit) ==") + for (n in listOf(100, 1000)) { + val seed = fixtures(n) + val arrivals = fixtures(n * 2).drop(n).map { (e, _) -> e } + val f = Filter(kinds = listOf(TextNoteEvent.KIND)) + + fun run(make: () -> Observable): () -> Int = + { + val subject = make() + seed.forEach { (e, note) -> subject.new(e, note) } + val notes = + arrivals.map { e -> + val note = Note(e.id) + note.event = e + e to note + } + val t0 = System.nanoTime() + notes.forEach { (e, note) -> subject.new(e, note) } + elapsed.addAndGet(System.nanoTime() - t0) + arrivals.size + } + + val c = benchInner("copy-on-write insert @$n", run { cow(f) }) + val s = benchInner("skip list insert @$n", run { skipList(f) }) + compare("insert @n=$n", c, s) + } + } + + private val elapsed = AtomicLong() + + /** Reports only the span the body accumulated into [elapsed], excluding its setup. */ + private fun benchInner( + label: String, + body: () -> Int, + reps: Int = 5, + warmups: Int = 2, + ): Double { + repeat(warmups) { + elapsed.set(0) + body() + } + val times = ArrayList(reps) + repeat(reps) { + elapsed.set(0) + val ops = body() + times.add(elapsed.get().toDouble() / ops) + } + times.sort() + val median = times[times.size / 2] + println(" %-44s %9.0f ns/op %11.0f ops/s".format(label, median, 1_000_000_000.0 / median)) + return median + } + + @Test + fun reDeliveryOfAnAlreadyListedNote() { + // The dominant steady-state call once a screen is warm: a newer version of an + // addressable, or the same note arriving from a second relay. + println("\n== re-delivery of a note already in the list ==") + for (n in listOf(100, 1000)) { + val fx = fixtures(n) + val f = Filter(kinds = listOf(TextNoteEvent.KIND), limit = n) + val reps = 100_000 + + val a = cow(f).also { s -> fx.forEach { (e, note) -> s.new(e, note) } } + val b = skipList(f).also { s -> fx.forEach { (e, note) -> s.new(e, note) } } + + val c = + bench("copy-on-write re-deliver @$n") { + for (i in 0 until reps) { + val (e, note) = fx[i % n] + a.new(e, note) + } + reps + } + val s = + bench("skip list re-deliver @$n") { + for (i in 0 until reps) { + val (e, note) = fx[i % n] + b.new(e, note) + } + reps + } + compare("re-deliver n=$n", c, s) + } + } + + // --------------------------------------------------------------- concurrent load + + private fun concurrently( + threads: Int, + ops: Int, + subject: Observable, + fx: List>, + ): Int { + val start = CountDownLatch(1) + val done = CountDownLatch(threads) + repeat(threads) { t -> + thread { + start.await() + var i = t + while (i < ops) { + val (e, note) = fx[i % fx.size] + subject.new(e, note) + i += threads + } + done.countDown() + } + } + start.countDown() + done.await() + return ops + } + + @Test + fun concurrentInsertsFromSeveralRelayThreads() { + // Worst case on purpose: these threads do nothing but insert. Real ingest spends most + // of its per-event budget on signature verification and parsing before reaching an + // observer, so actual contention on one filter is a fraction of this. + println("\n== concurrent inserts, threads doing nothing but inserting (worst case) ==") + val ops = 4_000 + val fx = fixtures(ops) + val f = Filter(kinds = listOf(TextNoteEvent.KIND), limit = 1000) + for (threads in listOf(1, 4, 8)) { + val c = bench("copy-on-write concurrent x$threads", reps = 3, warmups = 1) { concurrently(threads, ops, cow(f), fx) } + val s = bench("skip list concurrent x$threads", reps = 3, warmups = 1) { concurrently(threads, ops, skipList(f), fx) } + compare("concurrent x$threads", c, s) + } + println(" blackhole=${blackhole.get()}") + } + + /** + * [NoteListMatchingFilter] as it stood before the copy-on-write rewrite, verbatim apart from + * dropping the unused `atOnce`/`init` pair. Kept as the baseline these numbers are measured + * against, and as the oracle in [bothImplementationsAgree]. + * + * It is lock-free too -- `byId` is a ConcurrentHashMap, striped per key, and every write to + * `sorted` for an idHex happens inside that key's `compute` section -- which is why the + * comparison is about speed and portability, not about locking. + */ + private class SkipListReference( + private val filter: Filter, + private val update: (List) -> Unit, + ) : Observable { + private class Entry( + val note: Note, + val createdAt: Long, + val id: HexKey, + ) + + private val order = + Comparator { a, b -> + val byCreatedAt = b.createdAt.compareTo(a.createdAt) + if (byCreatedAt != 0) byCreatedAt else a.id.compareTo(b.id) + } + + private val sorted = ConcurrentSkipListSet(order) + private val byId = ConcurrentHashMap() + + private fun entryFor(note: Note) = Entry(note, note.createdAt() ?: Long.MIN_VALUE, note.event?.id ?: note.idHex) + + override fun new( + event: Event, + note: Note, + ) { + if (event is AddressableEvent && note !is AddressableNote) return + if (!filter.match(event)) return + + var added = false + byId.compute(note.idHex) { _, existing -> + existing ?: entryFor(note).also { + sorted.add(it) + added = true + } + } + if (!added) return + + val limit = filter.limit + if (limit != null && sorted.size > limit) { + sorted.pollLast()?.let { byId.remove(it.note.idHex, it) } + } + + update(snapshot()) + } + + override fun remove(note: Note) { + var removed = false + byId.compute(note.idHex) { _, existing -> + if (existing != null) { + sorted.remove(existing) + removed = true + } + null + } + if (removed) update(snapshot()) + } + + /** The skip list's iterator is only weakly consistent, so this had to strip duplicates. */ + private fun snapshot(): List { + val seen = HashSet() + return sorted.mapNotNull { e -> e.note.takeIf { seen.add(it.idHex) } } + } + } +} From aaeda4deead2b3eb9d014f31d95e1430cc0d1e49 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 19:08:35 +0000 Subject: [PATCH 24/29] test(commons): measure the observables' allocation, not just their speed Speed was measured; GC pressure was not, and on a phone young-gen churn is paid in jank rather than in CPU. Adds two cases to ObserverListBenchmark using HotSpot's per-thread allocation counter, so the numbers are exact rather than inferred from heap deltas. Against the ConcurrentSkipListSet implementation this replaced: - Re-delivery of an already listed note -- the commonest call in the app once a screen is warm -- allocates NOTHING. It is a load and a set lookup. The skip list's `byId.compute` allocated a capturing lambda and a boxed Ref.BooleanRef for the `added` flag: 56 bytes on every one. - A single-threaded insert allocates 1.2x less. - Under contention it inverts, for the same reason the timing does: a lost CAS discards the copy it just built. 2.04x more on 4 threads, 2.38x on 8. That tracks the measured slowdown (2.03x / 2.33x) closely enough to confirm the wasted copies are the whole of it. This corrects a guess made earlier in review that copy-on-write allocates more than the skip list did. Single-threaded, it allocates less; the "more" is contention only. Worth recording separately: neither implementation is cheap per insert. Both materialize the whole list on every write, so an observer holding a thousand notes allocates ~78 KB each time one arrives -- unchanged by the rewrite, and where the next win is if one is wanted. The lever is the per-write full-list materialization, not the locking. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8 --- .../observables/EventListMatchingFilter.kt | 11 ++ .../observables/NoteListMatchingFilter.kt | 11 ++ .../prodbench/ObserverListBenchmark.kt | 170 ++++++++++++++++++ 3 files changed, 192 insertions(+) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt index 303723c9fa..a445830f69 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt @@ -71,6 +71,17 @@ import kotlin.concurrent.atomics.ExperimentalAtomicApi * per-event budget verifying signatures and parsing before it reaches an observer, so the * contention window is a fraction of that. Worth revisiting if a profile ever says * otherwise. + * + * **Garbage**, measured the same way, since on a phone young-gen churn is paid in jank rather + * than in CPU: re-delivery allocates **nothing at all** (a load and a set lookup — where the + * skip list's `compute` allocated a capturing lambda and a boxed `Ref.BooleanRef`, 56 bytes on + * every one of the commonest call in the app), and a single-threaded insert allocates 1.2x less + * than the skip list did. Under contention it inverts for the same reason the timing does — a + * lost CAS discards the copy it just built — reaching 2.0x more on 4 threads and 2.4x on 8. + * + * What neither implementation is, is cheap per insert: both materialize the whole list on every + * write, so an observer holding a thousand notes allocates ~78 KB each time one arrives. That is + * unchanged by this rewrite and is where the next win is, if one is wanted — not in the locking. */ class EventListMatchingFilter( private val filter: Filter, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt index 136cff03cb..0fc40405be 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt @@ -71,6 +71,17 @@ import kotlin.concurrent.atomics.ExperimentalAtomicApi * per-event budget verifying signatures and parsing before it reaches an observer, so the * contention window is a fraction of that. Worth revisiting if a profile ever says * otherwise. + * + * **Garbage**, measured the same way, since on a phone young-gen churn is paid in jank rather + * than in CPU: re-delivery allocates **nothing at all** (a load and a set lookup — where the + * skip list's `compute` allocated a capturing lambda and a boxed `Ref.BooleanRef`, 56 bytes on + * every one of the commonest call in the app), and a single-threaded insert allocates 1.2x less + * than the skip list did. Under contention it inverts for the same reason the timing does — a + * lost CAS discards the copy it just built — reaching 2.0x more on 4 threads and 2.4x on 8. + * + * What neither implementation is, is cheap per insert: both materialize the whole list on every + * write, so an observer holding a thousand notes allocates ~78 KB each time one arrives. That is + * unchanged by this rewrite and is where the next win is, if one is wanted — not in the locking. */ class NoteListMatchingFilter( private val filter: Filter, diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/prodbench/ObserverListBenchmark.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/prodbench/ObserverListBenchmark.kt index bccf247fba..08cdd7f567 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/prodbench/ObserverListBenchmark.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/prodbench/ObserverListBenchmark.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.utils.EventFactory +import java.lang.management.ManagementFactory import java.util.Random import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentSkipListSet @@ -258,6 +259,175 @@ class ObserverListBenchmark { } } + // ------------------------------------------------------------ allocation / GC + + /** + * Bytes allocated per operation, which is the GC-pressure question: a phone pays for young-gen + * churn in jank, not just in CPU. Measured with HotSpot's per-thread allocation counter, so it + * is exact rather than inferred from heap deltas, and single-threaded so the counter is this + * work and nothing else. + */ + private fun allocatedBytes(): Long { + val bean = ManagementFactory.getThreadMXBean() as com.sun.management.ThreadMXBean + return bean.getThreadAllocatedBytes(Thread.currentThread().id) + } + + private fun measureAllocation( + label: String, + warmups: Int = 2, + body: () -> Int, + ): Double { + repeat(warmups) { body() } + val before = allocatedBytes() + val ops = body() + val bytes = (allocatedBytes() - before).toDouble() / ops + println(" %-44s %12.0f bytes/op".format(label, bytes)) + return bytes + } + + private fun compareBytes( + name: String, + cowBytes: Double, + skipBytes: Double, + ) { + val ratio = cowBytes / skipBytes + val verdict = + when { + ratio < 0.95 -> "copy-on-write allocates %.2fx LESS".format(1 / ratio) + ratio > 1.05 -> "copy-on-write allocates %.2fx MORE".format(ratio) + else -> "parity" + } + println(" -> $name: $verdict\n") + } + + @Test + fun allocationPerOperation() { + println("\n== bytes allocated per operation (GC pressure) ==") + + // (a) The re-delivery path: the dominant steady-state call once a screen is warm. + for (n in listOf(100, 1000)) { + val fx = fixtures(n) + val f = Filter(kinds = listOf(TextNoteEvent.KIND), limit = n) + val a = cow(f).also { s -> fx.forEach { (e, note) -> s.new(e, note) } } + val b = skipList(f).also { s -> fx.forEach { (e, note) -> s.new(e, note) } } + val reps = 50_000 + + val c = + measureAllocation("copy-on-write re-deliver @$n") { + for (i in 0 until reps) { + val (e, note) = fx[i % n] + a.new(e, note) + } + reps + } + val sk = + measureAllocation("skip list re-deliver @$n") { + for (i in 0 until reps) { + val (e, note) = fx[i % n] + b.new(e, note) + } + reps + } + compareBytes("re-deliver n=$n", c, sk) + } + + // (b) The insert path, where copy-on-write copies the list and the skip list does not. + for (n in listOf(100, 1000)) { + val seed = fixtures(n) + val arrivals = fixtures(n * 2).drop(n).map { (e, _) -> e } + val f = Filter(kinds = listOf(TextNoteEvent.KIND)) + + fun run(make: () -> Observable): () -> Int = + { + val subject = make() + seed.forEach { (e, note) -> subject.new(e, note) } + val notes = + arrivals.map { e -> + val note = Note(e.id) + note.event = e + e to note + } + val before = allocatedBytes() + notes.forEach { (e, note) -> subject.new(e, note) } + alloc.addAndGet(allocatedBytes() - before) + arrivals.size + } + + fun measureInner( + label: String, + body: () -> Int, + ): Double { + repeat(2) { + alloc.set(0) + body() + } + alloc.set(0) + val ops = body() + val bytes = alloc.get().toDouble() / ops + println(" %-44s %12.0f bytes/op".format(label, bytes)) + return bytes + } + + val c = measureInner("copy-on-write insert @$n", run { cow(f) }) + val sk = measureInner("skip list insert @$n", run { skipList(f) }) + compareBytes("insert @n=$n", c, sk) + } + println(" blackhole=${blackhole.get()}") + } + + private val alloc = AtomicLong() + + /** + * The one place copy-on-write could lose on allocation: a lost CAS throws away the copy it + * just built and retries, so contention multiplies the bytes per insert. The skip list has no + * such amplification. Summed across the worker threads, since the counter is per-thread. + */ + @Test + fun allocationUnderContention() { + println("\n== bytes allocated per insert under contention (CAS retry amplification) ==") + val ops = 4_000 + val fx = fixtures(ops) + val f = Filter(kinds = listOf(TextNoteEvent.KIND), limit = 1000) + + fun run( + threads: Int, + subject: Observable, + ): Double { + val bean = ManagementFactory.getThreadMXBean() as com.sun.management.ThreadMXBean + val total = AtomicLong() + val start = CountDownLatch(1) + val done = CountDownLatch(threads) + repeat(threads) { t -> + thread { + start.await() + val before = bean.getThreadAllocatedBytes(Thread.currentThread().id) + var i = t + while (i < ops) { + val (e, note) = fx[i % fx.size] + subject.new(e, note) + i += threads + } + total.addAndGet(bean.getThreadAllocatedBytes(Thread.currentThread().id) - before) + done.countDown() + } + } + start.countDown() + done.await() + return total.get().toDouble() / ops + } + + for (threads in listOf(1, 4, 8)) { + run(threads, cow(f)) + run(threads, skipList(f)) + val c = run(threads, cow(f)) + val sk = run(threads, skipList(f)) + println(" %-44s %12.0f bytes/op".format("copy-on-write insert x$threads", c)) + println(" %-44s %12.0f bytes/op".format("skip list insert x$threads", sk)) + compareBytes("insert x$threads", c, sk) + } + println(" blackhole=${blackhole.get()}") + } + // --------------------------------------------------------------- concurrent load private fun concurrently( From 5ad90115ffadfe94fec07ffa7e244ddeaa5801d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 20:15:50 +0000 Subject: [PATCH 25/29] fix: opt in to ExperimentalCoroutinesApi in calendar ViewModel tests `Dispatchers.setMain`/`resetMain` from kotlinx-coroutines-test are marked `@ExperimentalCoroutinesApi`, so the `@Before`/`@After` blocks in both calendar ViewModel tests emitted four "This declaration needs opt-in" warnings on every `:amethyst:compileFdroidDebugUnitTestKotlin`. Add a class-level `@OptIn(ExperimentalCoroutinesApi::class)`, matching what `NestViewModelTest` and `PollResultsViewModelTest` in `commons` already do for the same helpers. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RQSCURwPmwVfGR9Gard5JY --- .../amethyst/calendar/CalendarsViewModelFlowTest.kt | 2 ++ .../vitorpamplona/amethyst/calendar/CalendarsViewModelTest.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelFlowTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelFlowTest.kt index 1c05b77857..173db7e738 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelFlowTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelFlowTest.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.StateFlow @@ -65,6 +66,7 @@ import org.junit.Test * `LocalCache` is a process-wide object and JUnit's method order is hash-based, so every method * uses its own author key and asserts only over that key's events. */ +@OptIn(ExperimentalCoroutinesApi::class) class CalendarsViewModelFlowTest { // viewModelScope is Dispatchers.Main, and every flow is a WhileSubscribed stateIn, so nothing // runs until something subscribes. diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelTest.kt index 389b3ef5aa..770e0add94 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelTest.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsViewMode import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.startOfWeek import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.junit.After @@ -41,6 +42,7 @@ import java.time.YearMonth * The paging arithmetic the calendar lenses share, now that it lives on the screen's ViewModel * instead of being spelled out in each view's click handlers. */ +@OptIn(ExperimentalCoroutinesApi::class) class CalendarsViewModelTest { // The model's derived flows are `stateIn(viewModelScope, …)`, and stateIn launches its // sharing coroutine as soon as the property initializer runs — so simply CONSTRUCTING the From 2d907b9ec591de7bd8f8917f404e80c370d501fc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 20:18:00 +0000 Subject: [PATCH 26/29] test(amethyst): stop unit tests leaking coroutine scopes into each other MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Nip46ConsentInfoBuilderTest > anUncachedCounterpartyFallsBackToAShortenedNpub...` failed with `kotlinx.coroutines.test.UncaughtExceptionsBeforeTest` and passed when run alone. That exception never means what it reads like: `runTest` throws it from `TestScopeImpl.enter()`, *before* the test body runs, when `kotlinx-coroutines-test`'s process-wide `ExceptionCollector` has an exception banked from before this test started. The module runs in one JVM (no `forkEvery`), so a coroutine failing in a scope with no `CoroutineExceptionHandler` reaches the JVM-wide handler, is banked while no test is registered, and is replayed into whichever `runTest` starts next. Gradle runs classes in alphabetical FQN order and the failing method is that class's first (`MethodSorters.DEFAULT` orders by name hash), so the test named is a bystander: it is just the first `runTest` after `SplitterTest`, the only earlier class that uses `runTest` and therefore the one that arms the collector. Two tests here hand a real-dispatcher scope to production code and never cancel it, which is exactly how an exception gets banked: - `BlockedRelayListAddRelayTest` built a fresh `CoroutineScope(Dispatchers.IO + SupervisorJob())` per fixture. `BlockedRelayListState` starts an eager `stateIn` and a forever `collect` on whatever scope it is given, so every case left two coroutines NIP-44-decrypting for the rest of the JVM's life — including past `unmockkStatic(Looper::class)`, after which `checkNotInMainThread()` reads `null == null` as "the main thread" and throws. - `NwcInfoCacheTest` shared one `Dispatchers.Unconfined` scope and never cancelled it; `NwcInfoCache` hands its fetches to `Dispatchers.IO`, so that scope owns real background work too. Both now own their scope and cancel it in `@After` — before unmocking `Looper`, in the first case. Adds `LeakedCoroutineReporter`, registered as a `META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler` in the test source set. It prints any leaked coroutine failure, with its stack trace, at the moment it happens, so a recurrence names the test that leaked instead of the innocent one that reports it. It only reports and never claims the exception, so nothing is swallowed. Verified: the reporter fires on a deliberately leaked exception; the fdroid unit suite is green across 43 runs before the change (15 full suite, 20 of the 36-class prefix that runs ahead of the reported failure, 8 under CPU contention) and green again after it. The original race never reproduced here, so the specific leak it hit is not proven to be one of these two — the reporter is there to name it if it comes back. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SNhcNActYvvvGAPVqH4RRJ --- .../amethyst/LeakedCoroutineReporter.kt | 69 +++++++++++++++++++ .../nip47WalletConnect/NwcInfoCacheTest.kt | 14 ++++ .../BlockedRelayListAddRelayTest.kt | 28 ++++++-- ...tlinx.coroutines.CoroutineExceptionHandler | 1 + 4 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 amethyst/src/test/java/com/vitorpamplona/amethyst/LeakedCoroutineReporter.kt create mode 100644 amethyst/src/test/resources/META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/LeakedCoroutineReporter.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/LeakedCoroutineReporter.kt new file mode 100644 index 0000000000..1ad851a3ca --- /dev/null +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/LeakedCoroutineReporter.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR 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 kotlinx.coroutines.CoroutineExceptionHandler +import kotlin.coroutines.AbstractCoroutineContextElement +import kotlin.coroutines.CoroutineContext + +/** + * Prints every coroutine failure that reaches the JVM-wide handler during a unit-test run, + * together with the thread and coroutine context it came from. + * + * Registered through `META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler`, so + * `kotlinx.coroutines` hands it any exception no `CoroutineExceptionHandler` in the failing + * coroutine's own context claimed — i.e. exactly the ones leaked by a scope built as + * `CoroutineScope(Dispatchers.IO + SupervisorJob())` and never cancelled. + * + * Why this is worth a file: every test class in this module shares one JVM, and + * `kotlinx-coroutines-test` installs a global handler of its own that *collects* such + * exceptions. One arriving while no test is running is replayed into the next `runTest` to + * start — which fails as `UncaughtExceptionsBeforeTest` naming a test that had nothing to do + * with it, and passes when run alone. Without this reporter the only record of the real + * culprit is a suppressed exception buried in one report; with it, the leak is printed at the + * moment it happens, under the class that was actually running. + * + * Deliberately only reports: it does not signal the exception as handled, so + * `kotlinx-coroutines-test` still surfaces it and no genuine failure is swallowed. + * + * `ServiceLoader` order decides whether this runs before or after that collector, which only + * matters while a test is running — there the collector claims the exception and fails that very + * test with its own stack trace, so nothing is lost either way. Between tests the collector + * claims nothing, which is precisely the case this reporter exists for. + */ +class LeakedCoroutineReporter : + AbstractCoroutineContextElement(CoroutineExceptionHandler), + CoroutineExceptionHandler { + override fun handleException( + context: CoroutineContext, + exception: Throwable, + ) { + System.err.println( + "LEAKED COROUTINE EXCEPTION — a coroutine failed outside any test's scope.\n" + + " thread: ${Thread.currentThread().name}\n" + + " context: $context\n" + + " The test class printed alongside this is the one that was running, NOT " + + "necessarily the one that started the coroutine. Whichever test built the " + + "scope in the stack trace below must cancel it before it finishes.", + ) + exception.printStackTrace() + } +} diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcInfoCacheTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcInfoCacheTest.kt index 733bbd5149..ffa297752b 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcInfoCacheTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcInfoCacheTest.kt @@ -36,6 +36,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -45,7 +46,20 @@ import java.util.concurrent.atomic.AtomicInteger class NwcInfoCacheTest { private var clock = 1_000L + + /** + * Unconfined so a caller runs inline up to its first real suspension, but [NwcInfoCache] still + * hands its fetches to [Dispatchers.IO] — so this scope owns real background work and must not + * outlive the test. A fetch still running afterwards has no + * [kotlinx.coroutines.CoroutineExceptionHandler] in context, and anything it throws lands on + * the JVM-wide handler, where `runTest` reports it against whichever unrelated test class + * starts next. + */ private val scope = CoroutineScope(Dispatchers.Unconfined) + + @After + fun tearDown() = scope.cancel() + private val relay = RelayUrlNormalizer.normalizeOrNull("wss://relay.example.com")!! private fun uri(pubkey: String) = Nip47WalletConnect.Nip47URINorm(pubkey, relay, "secret") diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListAddRelayTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListAddRelayTest.kt index 43838456fc..843ae5a133 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListAddRelayTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListAddRelayTest.kt @@ -36,6 +36,7 @@ import io.mockk.unmockkStatic import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertEquals @@ -56,6 +57,16 @@ import org.junit.Test * [LocalCache]. */ class BlockedRelayListAddRelayTest { + /** + * Handed to every [Fixture] so [tearDown] can cancel it. [BlockedRelayListState] starts an + * eager `stateIn` and a forever `collect` on whatever scope it is handed, so a scope left alive + * here outlives the test: its coroutines keep decrypting on [Dispatchers.IO] with no + * [kotlinx.coroutines.CoroutineExceptionHandler] in context, and anything they throw reaches + * the JVM-wide handler — where `runTest` reports it against whichever *unrelated* test class + * happens to start next, as `UncaughtExceptionsBeforeTest`. + */ + private val stateScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val paid = RelayUrlNormalizer.normalize("wss://paid.example.com") private val alsoPaid = RelayUrlNormalizer.normalize("wss://other.example.com") @@ -70,10 +81,15 @@ class BlockedRelayListAddRelayTest { @After fun tearDown() { + // Before unmocking: a collector still running after Looper goes back to its + // unmocked stub reads `null == null` as "the main thread" and throws. + stateScope.cancel() unmockkStatic(Looper::class) } - private class Fixture { + private class Fixture( + scope: CoroutineScope, + ) { val keyPair = KeyPair() val signer = NostrSignerInternal(keyPair) val decryptionCache = BlockedRelayListDecryptionCache(signer) @@ -88,7 +104,7 @@ class BlockedRelayListAddRelayTest { signer = signer, cache = LocalCache, decryptionCache = decryptionCache, - scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), + scope = scope, settings = settings, ) @@ -101,7 +117,7 @@ class BlockedRelayListAddRelayTest { @Test fun blockingTheFirstRelayCreatesTheList() = runTest { - val f = Fixture() + val f = Fixture(stateScope) val event = f.state.addRelay(paid) @@ -113,7 +129,7 @@ class BlockedRelayListAddRelayTest { @Test fun blockingASecondRelayKeepsTheFirst() = runTest { - val f = Fixture() + val f = Fixture(stateScope) f.land(f.state.addRelay(paid)) val event = f.state.addRelay(alsoPaid) @@ -124,7 +140,7 @@ class BlockedRelayListAddRelayTest { @Test fun blockingAnAlreadyBlockedRelayDoesNotDuplicateIt() = runTest { - val f = Fixture() + val f = Fixture(stateScope) f.land(f.state.addRelay(paid)) val event = f.state.addRelay(paid) @@ -140,7 +156,7 @@ class BlockedRelayListAddRelayTest { @Test fun blockedRelaysStayInEncryptedPrivateTags() = runTest { - val f = Fixture() + val f = Fixture(stateScope) f.land(f.state.addRelay(paid)) val event = f.state.addRelay(alsoPaid) diff --git a/amethyst/src/test/resources/META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler b/amethyst/src/test/resources/META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler new file mode 100644 index 0000000000..724951f161 --- /dev/null +++ b/amethyst/src/test/resources/META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler @@ -0,0 +1 @@ +com.vitorpamplona.amethyst.LeakedCoroutineReporter From 309aa9d17a72135b992b6c29bdfec93a5c7aabef Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Sep 2026 16:30:34 -0400 Subject: [PATCH 27/29] fix: stop the QR scanner pumping the zoom in and out AutoZoomSweep started hunting after 1.2s without a decode and then ran a full zoom-in/zoom-out triangle every 3s, indefinitely, until something decoded or the user pinched. 1.2s is barely longer than it takes a person to aim, so in normal use the camera simply pumped: reported from the tablet as "this strange zooming in and out effect". The intent -- a code too small in frame is the commonest reason a scan fails -- does not survive the cost. It made the viewfinder look broken during the seconds the reader was still aiming, and by moving the field of view every 100ms it also corrupted the overlay geometry: the analysis pipeline runs a frame or two behind the preview, so the highlight was computed from a frame captured at one zoom level and drawn over a preview showing another. The mismatch was worst at the moment of the first detection, when zoomRatio snaps back to 1f while the camera physically takes time to follow. Zoom is now only ever written by a pinch or by Reset, so the viewfinder does nothing the reader did not ask for. The zoom chip's condition loses its autoZoomEnabled half and simply shows whenever the reader has zoomed in. Camera scanning re-verified end to end on the emulator after the removal. Co-Authored-By: Claude Opus 5 (1M context) --- .../qrcode/scanner/QrScannerDialog.kt | 45 ------------------- .../qrcode/scanner/QrScannerOverlay.kt | 2 +- .../loggedIn/qrcode/scanner/QrScannerState.kt | 32 ------------- 3 files changed, 1 insertion(+), 78 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt index 88a373990e..0b133935ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt @@ -106,11 +106,9 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.util.concurrent.Executors import java.util.concurrent.TimeUnit -import kotlin.math.min /** What the caller did with a decoded payload — and therefore what the scanner does next. */ enum class ScanOutcome { @@ -350,8 +348,6 @@ private fun QrCameraScanner( snapshotFlow { state.zoomRatio }.collect { runCatching { control.setZoomRatio(it) } } } - AutoZoomSweep(state = state, enabled = camera != null) - LaunchedEffect(state.notice) { if (state.notice != null) { delay(NOTICE_DURATION_MS) @@ -394,7 +390,6 @@ private fun QrCameraScanner( .pointerInput(Unit) { detectTransformGestures { _, _, zoom, _ -> if (zoom != 1f) { - state.onPinch() state.zoomRatio = (state.zoomRatio * zoom).coerceIn(1f, maxOf(1f, state.maxZoomRatio)) } } @@ -503,44 +498,6 @@ private fun ScanOverlayCanvas( } } -/** - * Sweeps the zoom while nothing is decoding. - * - * A code too small in frame to resolve is the single most common reason a scan fails, and no - * amount of decoder tuning fixes it — there are not enough pixels per module to read. Rather than - * leave the user to work that out and walk closer, the camera pushes in and back out on its own. - * It stops for good once the user pinches: they have taken over. - */ -@Composable -private fun AutoZoomSweep( - state: QrScannerState, - enabled: Boolean, -) { - LaunchedEffect(enabled, state.autoZoomEnabled) { - if (!enabled || !state.autoZoomEnabled) return@LaunchedEffect - - var sweep = 0f - while (isActive) { - delay(AUTO_ZOOM_TICK_MS) - - if (state.msSinceLastDetection < QrScannerState.AUTO_ZOOM_AFTER_MS) { - if (sweep != 0f) { - sweep = 0f - state.zoomRatio = 1f - } - continue - } - - val ceiling = min(state.maxZoomRatio, QrScannerState.AUTO_ZOOM_MAX) - if (ceiling <= 1.01f) continue - - sweep = (sweep + AUTO_ZOOM_TICK_MS.toFloat() / AUTO_ZOOM_PERIOD_MS) % 1f - val triangle = if (sweep < 0.5f) sweep * 2f else (1f - sweep) * 2f - state.zoomRatio = 1f + triangle * (ceiling - 1f) - } - } -} - /** The visible code nearest the tap, or null when the tap was not on one. */ private fun pickCandidateAt( tap: Offset, @@ -620,8 +577,6 @@ private val ANALYSIS_RESOLUTION = ResolutionStrategy(Size(1280, 720), ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER), ).build() -private const val AUTO_ZOOM_TICK_MS = 100L -private const val AUTO_ZOOM_PERIOD_MS = 3_000f private const val NOTICE_DURATION_MS = 3_000L private const val FOCUS_RING_DURATION_MS = 800L private const val FOCUS_AUTO_CANCEL_SECONDS = 4L diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerOverlay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerOverlay.kt index d420d78867..33a127f1df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerOverlay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerOverlay.kt @@ -187,7 +187,7 @@ fun QrScannerControls( ) } - if (!state.autoZoomEnabled && state.zoomRatio > 1.05f) { + if (state.zoomRatio > 1.05f) { ZoomChip( zoomRatio = state.zoomRatio, onReset = { state.resetZoom() }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerState.kt index 10c56702cd..1767e27e01 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerState.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -63,13 +62,6 @@ class QrScannerState { var zoomRatio by mutableFloatStateOf(1f) var maxZoomRatio by mutableFloatStateOf(1f) - /** - * Auto-zoom stops for good the first time the user pinches. Someone who has framed the shot - * themselves does not want the camera arguing about it. - */ - var autoZoomEnabled by mutableStateOf(true) - private set - /** A payload we decoded but the caller could not use; drives the explain-what-happened sheet. */ var rejected by mutableStateOf(null) @@ -89,16 +81,6 @@ class QrScannerState { private var lastSubmittedAt = 0L private var darkSinceMs = 0L - /** Milliseconds since anything at all was decoded — what auto-zoom watches. */ - var msSinceLastDetection by mutableLongStateOf(0L) - private set - - private var lastDetectionMs = 0L - - fun onPinch() { - autoZoomEnabled = false - } - fun resetZoom() { zoomRatio = 1f } @@ -121,11 +103,6 @@ class QrScannerState { // whether or not the camera is busy reading something else. if (sequence.dropIfStale(nowMs)) sequenceProgress = null - // Seed the clock on the first frame. Left at zero, the very first empty frame would read - // as "nothing decoded since the epoch" and send auto-zoom hunting before the user has had - // a chance to aim. - if (lastDetectionMs == 0L) lastDetectionMs = nowMs - // Nothing is decided while the "we can't open this" sheet is up: the user is reading it, // and the offending code is very probably still sitting in front of the lens. if (rejected != null) { @@ -136,12 +113,9 @@ class QrScannerState { val found = scan.results.distinctBy { it.text } if (found.isEmpty()) { candidates = emptyList() - msSinceLastDetection = nowMs - lastDetectionMs return null } - lastDetectionMs = nowMs - msSinceLastDetection = 0 candidates = found // A multi-part code is never complete on its first part, so it can't be a single answer. @@ -232,11 +206,5 @@ class QrScannerState { /** Don't offer the torch for a thumb over the lens or a moment of shadow. */ const val DARK_DWELL_MS = 1_000L - - /** How long with nothing decoded before auto-zoom starts hunting. */ - const val AUTO_ZOOM_AFTER_MS = 1_200L - - /** Ceiling for the auto-zoom sweep — past this, focus and shake beat the extra reach. */ - const val AUTO_ZOOM_MAX = 2.5f } } From 1f18e85863fd846b58e3f46d78fdee01e4eeab28 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Sep 2026 17:15:54 -0400 Subject: [PATCH 28/29] fix: read the query of an opaque uri on JVM and Android `amethyst+walletconnect:dlnwc?value=...` came back as "Amethyst received a URI to open but that uri was invalid", while the `//` spelling of the very same link worked. isWalletConnectRoute() accepts three spellings; only two survived the parse behind it. A uri whose scheme is followed by anything but `/` is *opaque* to java.net.URI: it reports no query at all and folds `dlnwc?value=...` into one scheme-specific part. The linux actual of UriParser -- a hand-rolled parser -- looks for `?` wherever it sits, so the actuals disagreed and the JVM one was the one losing data. Nip47DeepLink.parseCallbackValue rides the same call, so NWC callbacks were exposed to it too. The query is now recovered from the scheme-specific part when the uri is opaque. The fragment needs no such rescue: java.net.URI does parse `#...` off an opaque uri, and a test pins that. UriParserTest states the contract for every actual. The existing UriToRouteTest only covered the bare `dlnwc?value=` form -- the spelling that already worked -- which is how this went unnoticed; it now covers all three, and fails on the one-colon form without this change. Co-Authored-By: Claude Opus 5 (1M context) --- .../amethyst/ui/UriToRouteTest.kt | 18 ++++ .../quartz/utils/UriParserTest.kt | 97 +++++++++++++++++++ .../quartz/utils/UriParser.jvmAndroid.kt | 22 ++++- 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/UriParserTest.kt diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt index 67425d5405..95b20c73c2 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt @@ -93,6 +93,24 @@ class UriToRouteTest { ) } + @Test + fun walletConnectDeepLinksRouteInEverySpellingIsWalletConnectRouteAccepts() { + // isWalletConnectRoute() accepts three spellings, so all three have to survive the parse + // behind it. They did not: `java.net.URI` calls a scheme followed by anything but `/` an + // *opaque* uri and reports it as having no query at all, so the one-colon form lost its + // `value=` and came back to the user as "that uri was invalid" -- while the `//` spelling + // of the very same link worked. Only the bare form was covered here before. + val encoded = URLEncoder.encode(NWC_URI, Charsets.UTF_8.name()) + + listOf( + "dlnwc?value=$encoded", + "amethyst+walletconnect:dlnwc?value=$encoded", + "amethyst+walletconnect://dlnwc?value=$encoded", + ).forEach { deepLink -> + assertEquals(deepLink, Route.WalletAddNwc(NWC_URI), uriToRoute(deepLink, account)) + } + } + @Test fun theLauncherShortcutOpensTheScannerDirectly() { // res/xml/shortcuts.xml fires `amethyst:scanqr`. If this stops resolving, long-pressing the diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/UriParserTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/UriParserTest.kt new file mode 100644 index 0000000000..a76776ad7f --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/UriParserTest.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.utils + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +/** + * The contract every actual of [UriParser] has to keep. + * + * The interesting case is the *opaque* URI -- a scheme followed by anything but `/`. The JVM and + * Android actual delegates to `java.net.URI`, which declares such a URI to have no query at all + * and folds `dlnwc?value=...` into one scheme-specific part, while the hand-rolled actuals look + * for `?` wherever it sits. That disagreement is what made a wallet-connect deep link report + * "that uri was invalid" on Android while the `//` spelling of the same link worked. + */ +class UriParserTest { + private val nwc = "nostr+walletconnect://abc?relay=wss://r.example&secret=s1" + + @Test + fun readsTheQueryOfAnOpaqueUri() { + val parser = UriParser("amethyst+walletconnect:dlnwc?value=${encode(nwc)}") + + assertEquals(nwc, parser.getQueryParameter("value")?.firstOrNull()) + } + + /** The spelling that already worked, so the rescue above cannot have broken it. */ + @Test + fun readsTheQueryOfAHierarchicalUri() { + val parser = UriParser("amethyst+walletconnect://dlnwc?value=${encode(nwc)}") + + assertEquals(nwc, parser.getQueryParameter("value")?.firstOrNull()) + } + + /** No scheme at all, which is how the callback arrives from some wallets. */ + @Test + fun readsTheQueryOfASchemelessUri() { + val parser = UriParser("dlnwc?value=${encode(nwc)}") + + assertEquals(nwc, parser.getQueryParameter("value")?.firstOrNull()) + } + + @Test + fun opaqueUriWithoutAQueryHasNoParameters() { + val parser = UriParser("cashu:sometoken") + + assertNull(parser.getQueryParameter("value")) + assertEquals(emptySet(), parser.queryParameterNames()) + } + + @Test + fun namesEveryParameterOfAnOpaqueUri() { + val parser = UriParser("bunker:pubkeyhex?relay=wss%3A%2F%2Fr.example&secret=s1") + + assertEquals(setOf("relay", "secret"), parser.queryParameterNames()) + assertEquals("wss://r.example", parser.getQueryParameter("relay")?.firstOrNull()) + assertEquals("s1", parser.getQueryParameter("secret")?.firstOrNull()) + } + + /** A fragment is parsed off an opaque URI by every actual already; keep it that way. */ + @Test + fun keepsTheFragmentOfAnOpaqueUri() { + val parser = UriParser("scheme:thing?a=1#b=2") + + assertEquals("1", parser.getQueryParameter("a")?.firstOrNull()) + assertEquals(mapOf("b" to "2"), parser.fragments()) + } + + private fun encode(value: String) = + value + .replace("%", "%25") + .replace("+", "%2B") + .replace(":", "%3A") + .replace("/", "%2F") + .replace("?", "%3F") + .replace("&", "%26") + .replace("=", "%3D") +} diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/UriParser.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/UriParser.jvmAndroid.kt index 72057834ef..a961dbb53f 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/UriParser.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/UriParser.jvmAndroid.kt @@ -28,8 +28,28 @@ actual class UriParser actual constructor( ) { private val myUri = URI.create(uri) + /** + * The query string, including the one [java.net.URI] refuses to find. + * + * A URI whose scheme is followed by anything other than `/` is *opaque* to `java.net.URI`: + * `amethyst+walletconnect:dlnwc?value=...` has no query at all as far as it is concerned, and + * the whole `dlnwc?value=...` is one scheme-specific part. Every other actual of this class + * looks for `?` wherever it sits, so on JVM and Android alone the parameters of an opaque URI + * silently vanished -- which is how a perfectly valid wallet-connect deep link came back as + * "that uri was invalid", while the `//` spelling of the very same link worked. + * + * The fragment needs no such rescue: `java.net.URI` does parse `#...` off an opaque URI. + */ + private val rawQuery: String? = + myUri.rawQuery + ?: myUri + .takeIf { it.isOpaque } + ?.rawSchemeSpecificPart + ?.substringAfter('?', "") + ?.ifBlank { null } + private val queryParameters: Map> by lazy { - myUri.rawQuery?.ifBlank { null }?.let { query -> + rawQuery?.ifBlank { null }?.let { query -> val params = mutableMapOf>() query.split('&').forEach { paramValue -> From 71b1438c41834826f26b30769c885a162feb0c95 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Sep 2026 17:16:07 -0400 Subject: [PATCH 29/29] fix: say why a bunker:// address cannot sign in, instead of "invalid key" Pasting a `bunker://` address into the login field reported "Invalid key: Could not sign in: Invalid hex bunker://...", which reads as "you mistyped it" for a string the user pasted correctly. It is not a key Amethyst failed to read, it is a sign-in method Android does not have. `AccountSettings.isWriteable()` is `privKey != null || externalSignerPackageName != null`: the only remote signing it can persist is an external signer app. The nip46* settings are Amethyst acting *as* a bunker for other apps -- the opposite direction -- and BunkerLoginUseCase is referenced only by desktopApp. So the login screen now says so. `nostrconnect://` is guarded the same way and for the same reason: it is an app asking to connect to your signer, and belongs on the NIP-46 signer screen. Both matched the wording the scanner's outcome sheet already uses. This does not add bunker sign-in to Android; it stops misreporting its absence. Whether to implement it is a separate decision. Verified on device: the add-account login screen shows the explanation. Co-Authored-By: Claude Opus 5 (1M context) --- .../screen/loggedOff/login/LoginViewModel.kt | 15 ++++ amethyst/src/main/res/values/strings.xml | 2 + .../login/LoginViewModelKeyKindTest.kt | 81 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModelKeyKindTest.kt 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 ef7e79f0bf..720f4ae845 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 @@ -128,6 +128,21 @@ class LoginViewModel : ViewModel() { return false } + // Caught here rather than left to the key parser. Neither of these is a key, so both used + // to fall through to the hex branch and come back as "Invalid key: ... Invalid hex + // bunker://...", which reads as "you mistyped it" for a string the user pasted correctly. + // Amethyst for Android is the bunker, never the bunker's client: the only remote signing it + // can persist is an external signer app (see AccountSettings.isWriteable). + val trimmedKey = key.text.trim() + if (trimmedKey.startsWith("bunker:", ignoreCase = true)) { + errorManager.error(R.string.login_bunker_not_supported) + return false + } + if (trimmedKey.startsWith("nostrconnect:", ignoreCase = true)) { + errorManager.error(R.string.login_nostrconnect_not_supported) + return false + } + if (needsPassword && password.text.isBlank()) { errorManager.error(R.string.password_is_required) return false diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index db5d7c8a76..22f30b780d 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -123,6 +123,8 @@ Follow back Hide Password Invalid key + Amethyst for Android can\'t sign in with a bunker:// address. It hands these out so other apps can ask it to sign, but it cannot sign in with one. Use an external signer app, or Amethyst Desktop. + That\'s an app asking to connect to your signer, not a way to sign in. Sign in first, then open it from the NIP-46 signer screen. Invalid key: %1$s Password is required Key is required diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModelKeyKindTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModelKeyKindTest.kt new file mode 100644 index 0000000000..839d80dc38 --- /dev/null +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModelKeyKindTest.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.loggedOff.login + +import androidx.compose.ui.text.input.TextFieldValue +import com.vitorpamplona.amethyst.R +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Test + +/** + * Amethyst for Android is the bunker, never the bunker's client. + * + * `AccountSettings.isWriteable()` is `privKey != null || externalSignerPackageName != null`: the + * only remote signing it can persist is an external signer app. A `bunker://` address pasted into + * the key field is therefore not a key it failed to read, it is a sign-in method it does not have + * — but it used to fall through to the hex branch and come back as + * "Invalid key: Could not sign in: Invalid hex bunker://…", which reads as "you mistyped it" for a + * string the user pasted correctly. + */ +class LoginViewModelKeyKindTest { + private fun viewModelWithKey(text: String) = + LoginViewModel().apply { + acceptedTerms = true + key = TextFieldValue(text) + } + + private fun errorResIdOf(model: LoginViewModel) = (model.errorManager.error as? LoginErrorManager.SingleErrorMsg)?.errorResId + + @Test + fun aBunkerAddressIsRefusedAsAnUnsupportedMethodNotAsABadKey() { + val model = viewModelWithKey("bunker://${"a".repeat(64)}?relay=wss%3A%2F%2Fr.example&secret=s1") + + assertFalse(model.checkCanLogin()) + assertEquals(R.string.login_bunker_not_supported, errorResIdOf(model)) + } + + /** Same defect, same field: a connect offer is not a sign-in method either. */ + @Test + fun aNostrConnectOfferIsRefusedWithItsOwnExplanation() { + val model = viewModelWithKey("nostrconnect://${"b".repeat(64)}?relay=wss%3A%2F%2Fr.example&secret=s1") + + assertFalse(model.checkCanLogin()) + assertEquals(R.string.login_nostrconnect_not_supported, errorResIdOf(model)) + } + + /** Pasted from a chat app, which loves to add a trailing space. */ + @Test + fun surroundingWhitespaceDoesNotHideTheExplanation() { + val model = viewModelWithKey(" bunker://${"a".repeat(64)}?secret=s1 ") + + assertFalse(model.checkCanLogin()) + assertEquals(R.string.login_bunker_not_supported, errorResIdOf(model)) + } + + /** An npub is still a key, so the guard must not swallow the ordinary path. */ + @Test + fun anOrdinaryKeyStillPassesTheCheck() { + val model = viewModelWithKey("npub1${"q".repeat(58)}") + + assertEquals(true, model.checkCanLogin()) + } +}