From 7de681354375aa6fdf6b1a73df0d7da810b67494 Mon Sep 17 00:00:00 2001 From: Aleksandar Ilic Date: Wed, 23 Aug 2023 19:39:23 +0200 Subject: [PATCH] Improve caching and updating user account; Improve follow/unfollow business logic; Improve local relays management; Implement MissingRelaysException; Show missing relays configuration error message; Show follow/unfollow error messages; --- .../net/primal/android/auth/AuthRepository.kt | 3 +- .../android/discuss/feed/FeedContract.kt | 1 + .../primal/android/discuss/feed/FeedScreen.kt | 2 +- .../android/discuss/feed/FeedViewModel.kt | 7 + .../android/discuss/post/NewPostContract.kt | 8 +- .../android/discuss/post/NewPostScreen.kt | 15 +- .../android/discuss/post/NewPostViewModel.kt | 7 +- .../explore/feed/ExploreFeedContract.kt | 2 + .../android/explore/feed/ExploreFeedScreen.kt | 1 + .../explore/feed/ExploreFeedViewModel.kt | 7 + .../android/networking/relays/RelayPool.kt | 13 +- .../relays/errors/MissingRelaysException.kt | 3 + .../profile/details/ProfileContract.kt | 3 + .../android/profile/details/ProfileScreen.kt | 3 + .../profile/details/ProfileViewModel.kt | 34 +++-- .../repository/LatestFollowingResolver.kt | 45 ------ .../profile/repository/ProfileRepository.kt | 52 ++++--- .../settings/repository/SettingsRepository.kt | 2 +- .../primal/android/thread/ThreadContract.kt | 1 + .../net/primal/android/thread/ThreadScreen.kt | 1 + .../primal/android/thread/ThreadViewModel.kt | 9 ++ .../android/user/accounts/BootstrapRelays.kt | 1 - .../user/accounts/UserAccountFetcher.kt | 39 +----- .../user/accounts/UserAccountsStore.kt | 12 +- .../primal/android/user/domain/UserAccount.kt | 1 - .../android/user/domain/UserAccountExt.kt | 1 - .../android/user/repository/UserRepository.kt | 61 +++------ .../android/user/updater/UserDataUpdater.kt | 2 +- .../wallet/repository/ZapRepository.kt | 2 +- app/src/main/res/values/strings.xml | 3 + .../repository/LatestFollowingResolverTest.kt | 128 ------------------ .../user/accounts/UserAccountFetcherTest.kt | 25 +--- .../user/accounts/UserAccountsStoreTest.kt | 4 +- 33 files changed, 168 insertions(+), 330 deletions(-) create mode 100644 app/src/main/kotlin/net/primal/android/networking/relays/errors/MissingRelaysException.kt delete mode 100644 app/src/main/kotlin/net/primal/android/profile/repository/LatestFollowingResolver.kt delete mode 100644 app/src/test/kotlin/net/primal/android/profile/repository/LatestFollowingResolverTest.kt diff --git a/app/src/main/kotlin/net/primal/android/auth/AuthRepository.kt b/app/src/main/kotlin/net/primal/android/auth/AuthRepository.kt index 914d1bbb2..3ee1e05a4 100644 --- a/app/src/main/kotlin/net/primal/android/auth/AuthRepository.kt +++ b/app/src/main/kotlin/net/primal/android/auth/AuthRepository.kt @@ -15,7 +15,8 @@ class AuthRepository @Inject constructor( suspend fun login(nostrKey: String): String { val pubkey = credentialsStore.save(nostrKey) - userRepository.fetchAndUpsertUserAccount(userId = pubkey) + userRepository.createNewUserAccount(userId = pubkey) + userRepository.fetchAndUpdateUserAccount(userId = pubkey) activeAccountStore.setActiveUserId(pubkey) return pubkey } diff --git a/app/src/main/kotlin/net/primal/android/discuss/feed/FeedContract.kt b/app/src/main/kotlin/net/primal/android/discuss/feed/FeedContract.kt index b121d5234..e6f647ce4 100644 --- a/app/src/main/kotlin/net/primal/android/discuss/feed/FeedContract.kt +++ b/app/src/main/kotlin/net/primal/android/discuss/feed/FeedContract.kt @@ -23,6 +23,7 @@ interface FeedContract { data class FailedToPublishZapEvent(val cause: Throwable) : FeedError() data class FailedToPublishRepostEvent(val cause: Throwable) : FeedError() data class FailedToPublishLikeEvent(val cause: Throwable) : FeedError() + data class MissingRelaysConfiguration(val cause: Throwable) : FeedError() } } diff --git a/app/src/main/kotlin/net/primal/android/discuss/feed/FeedScreen.kt b/app/src/main/kotlin/net/primal/android/discuss/feed/FeedScreen.kt index b1b2a8047..99b1228e9 100644 --- a/app/src/main/kotlin/net/primal/android/discuss/feed/FeedScreen.kt +++ b/app/src/main/kotlin/net/primal/android/discuss/feed/FeedScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -250,6 +249,7 @@ private fun ErrorHandler( is FeedError.FailedToPublishZapEvent -> context.getString(R.string.post_action_zap_failed) is FeedError.FailedToPublishLikeEvent -> context.getString(R.string.post_action_like_failed) is FeedError.FailedToPublishRepostEvent -> context.getString(R.string.post_action_repost_failed) + is FeedError.MissingRelaysConfiguration -> context.getString(R.string.app_missing_relays_config) else -> null } diff --git a/app/src/main/kotlin/net/primal/android/discuss/feed/FeedViewModel.kt b/app/src/main/kotlin/net/primal/android/discuss/feed/FeedViewModel.kt index a3eb35f09..77b2cb876 100644 --- a/app/src/main/kotlin/net/primal/android/discuss/feed/FeedViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/discuss/feed/FeedViewModel.kt @@ -25,6 +25,7 @@ import net.primal.android.discuss.feed.FeedContract.UiState.FeedError import net.primal.android.feed.repository.FeedRepository import net.primal.android.feed.repository.PostRepository import net.primal.android.navigation.feedDirective +import net.primal.android.networking.relays.errors.MissingRelaysException import net.primal.android.networking.relays.errors.NostrPublishException import net.primal.android.user.accounts.active.ActiveAccountStore import net.primal.android.user.accounts.active.ActiveUserAccountState @@ -159,6 +160,8 @@ class FeedViewModel @Inject constructor( ) } catch (error: NostrPublishException) { setErrorState(error = FeedError.FailedToPublishLikeEvent(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = FeedError.MissingRelaysConfiguration(error)) } } @@ -171,6 +174,8 @@ class FeedViewModel @Inject constructor( ) } catch (error: NostrPublishException) { setErrorState(error = FeedError.FailedToPublishRepostEvent(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = FeedError.MissingRelaysConfiguration(error)) } } @@ -195,6 +200,8 @@ class FeedViewModel @Inject constructor( setErrorState(error = FeedError.FailedToPublishZapEvent(error)) } catch (error: NostrPublishException) { setErrorState(error = FeedError.FailedToPublishZapEvent(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = FeedError.MissingRelaysConfiguration(error)) } catch (error: ZapRepository.InvalidZapRequestException) { setErrorState(error = FeedError.InvalidZapRequest(error)) } diff --git a/app/src/main/kotlin/net/primal/android/discuss/post/NewPostContract.kt b/app/src/main/kotlin/net/primal/android/discuss/post/NewPostContract.kt index 2960ff9fa..c3b36fc1e 100644 --- a/app/src/main/kotlin/net/primal/android/discuss/post/NewPostContract.kt +++ b/app/src/main/kotlin/net/primal/android/discuss/post/NewPostContract.kt @@ -5,10 +5,14 @@ interface NewPostContract { data class UiState( val preFillContent: String? = null, val publishing: Boolean = false, - val error: PublishError? = null, + val error: NewPostError? = null, val activeAccountAvatarUrl: String? = null, ) { - data class PublishError(val cause: Throwable?) + sealed class NewPostError { + data class PublishError(val cause: Throwable?) : NewPostError() + data class MissingRelaysConfiguration(val cause: Throwable) : NewPostError() + } + } sealed class UiEvent { diff --git a/app/src/main/kotlin/net/primal/android/discuss/post/NewPostScreen.kt b/app/src/main/kotlin/net/primal/android/discuss/post/NewPostScreen.kt index e69ecc747..dafccaf07 100644 --- a/app/src/main/kotlin/net/primal/android/discuss/post/NewPostScreen.kt +++ b/app/src/main/kotlin/net/primal/android/discuss/post/NewPostScreen.kt @@ -36,8 +36,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import net.primal.android.R import net.primal.android.core.compose.AvatarThumbnailListItemImage -import net.primal.android.core.compose.button.PrimalLoadingButton import net.primal.android.core.compose.PrimalTopAppBar +import net.primal.android.core.compose.button.PrimalLoadingButton +import net.primal.android.discuss.post.NewPostContract.UiState.NewPostError import net.primal.android.theme.AppTheme @Composable @@ -161,14 +162,20 @@ fun NewPostScreen( @Composable private fun NewPostPublishErrorHandler( - error: NewPostContract.UiState.PublishError?, + error: NewPostError?, snackbarHostState: SnackbarHostState, ) { val context = LocalContext.current LaunchedEffect(error ?: true) { - if (error != null) { + val errorMessage = when (error) { + is NewPostError.MissingRelaysConfiguration -> context.getString(R.string.app_missing_relays_config) + is NewPostError.PublishError -> context.getString(R.string.new_post_nostr_publish_error) + else -> null + } + + if (errorMessage != null) { snackbarHostState.showSnackbar( - message = context.getString(R.string.new_post_nostr_publish_error), + message = errorMessage, duration = SnackbarDuration.Short, ) } diff --git a/app/src/main/kotlin/net/primal/android/discuss/post/NewPostViewModel.kt b/app/src/main/kotlin/net/primal/android/discuss/post/NewPostViewModel.kt index 21e0e9469..76d3e57c0 100644 --- a/app/src/main/kotlin/net/primal/android/discuss/post/NewPostViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/discuss/post/NewPostViewModel.kt @@ -18,6 +18,7 @@ import net.primal.android.discuss.post.NewPostContract.UiEvent import net.primal.android.discuss.post.NewPostContract.UiState import net.primal.android.feed.repository.PostRepository import net.primal.android.navigation.newPostPreFillContent +import net.primal.android.networking.relays.errors.MissingRelaysException import net.primal.android.networking.relays.errors.NostrPublishException import net.primal.android.nostr.ext.parseEventTags import net.primal.android.nostr.ext.parseHashtagTags @@ -81,13 +82,15 @@ class NewPostViewModel @Inject constructor( ) sendEffect(SideEffect.PostPublished) } catch (error: NostrPublishException) { - setErrorState(error = UiState.PublishError(cause = error.cause)) + setErrorState(error = UiState.NewPostError.PublishError(cause = error.cause)) + } catch (error: MissingRelaysException) { + setErrorState(error = UiState.NewPostError.MissingRelaysConfiguration(cause = error)) } finally { setState { copy(publishing = false) } } } - private fun setErrorState(error: UiState.PublishError) { + private fun setErrorState(error: UiState.NewPostError) { setState { copy(error = error) } viewModelScope.launch { delay(2.seconds) diff --git a/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedContract.kt b/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedContract.kt index 680fabcf5..ddc567703 100644 --- a/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedContract.kt +++ b/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedContract.kt @@ -20,6 +20,8 @@ interface ExploreFeedContract { data class FailedToPublishZapEvent(val cause: Throwable) : ExploreFeedError() data class FailedToPublishRepostEvent(val cause: Throwable) : ExploreFeedError() data class FailedToPublishLikeEvent(val cause: Throwable) : ExploreFeedError() + data class MissingRelaysConfiguration(val cause: Throwable) : ExploreFeedError() + } } diff --git a/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedScreen.kt b/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedScreen.kt index 5ea39d73d..b80f66ac6 100644 --- a/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedScreen.kt +++ b/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedScreen.kt @@ -186,6 +186,7 @@ private fun ErrorHandler( is ExploreFeedError.FailedToPublishZapEvent -> context.getString(R.string.post_action_zap_failed) is ExploreFeedError.FailedToPublishLikeEvent -> context.getString(R.string.post_action_like_failed) is ExploreFeedError.FailedToPublishRepostEvent -> context.getString(R.string.post_action_repost_failed) + is ExploreFeedError.MissingRelaysConfiguration -> context.getString(R.string.app_missing_relays_config) null -> return@LaunchedEffect } diff --git a/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedViewModel.kt b/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedViewModel.kt index 88d4ac2f9..593fe1e0e 100644 --- a/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedViewModel.kt @@ -22,6 +22,7 @@ import net.primal.android.explore.feed.ExploreFeedContract.UiState.ExploreFeedEr import net.primal.android.feed.repository.FeedRepository import net.primal.android.feed.repository.PostRepository import net.primal.android.navigation.searchQuery +import net.primal.android.networking.relays.errors.MissingRelaysException import net.primal.android.networking.relays.errors.NostrPublishException import net.primal.android.user.accounts.active.ActiveAccountStore import net.primal.android.user.accounts.active.ActiveUserAccountState @@ -115,6 +116,8 @@ class ExploreFeedViewModel @Inject constructor( ) } catch (error: NostrPublishException) { setErrorState(error = ExploreFeedError.FailedToPublishLikeEvent(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = ExploreFeedError.MissingRelaysConfiguration(error)) } } @@ -127,6 +130,8 @@ class ExploreFeedViewModel @Inject constructor( ) } catch (error: NostrPublishException) { setErrorState(error = ExploreFeedError.FailedToPublishRepostEvent(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = ExploreFeedError.MissingRelaysConfiguration(error)) } } @@ -151,6 +156,8 @@ class ExploreFeedViewModel @Inject constructor( setErrorState(error = ExploreFeedError.FailedToPublishZapEvent(error)) } catch (error: NostrPublishException) { setErrorState(error = ExploreFeedError.FailedToPublishZapEvent(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = ExploreFeedError.MissingRelaysConfiguration(error)) } catch (error: ZapRepository.InvalidZapRequestException) { setErrorState(error = ExploreFeedError.InvalidZapRequest(error)) } diff --git a/app/src/main/kotlin/net/primal/android/networking/relays/RelayPool.kt b/app/src/main/kotlin/net/primal/android/networking/relays/RelayPool.kt index a319c4960..9f6f8bbb0 100644 --- a/app/src/main/kotlin/net/primal/android/networking/relays/RelayPool.kt +++ b/app/src/main/kotlin/net/primal/android/networking/relays/RelayPool.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import net.primal.android.networking.UserAgentProvider +import net.primal.android.networking.relays.errors.MissingRelaysException import net.primal.android.networking.relays.errors.NostrPublishException import net.primal.android.networking.sockets.NostrIncomingMessage import net.primal.android.networking.sockets.NostrSocketClient @@ -71,10 +72,18 @@ class RelayPool @Inject constructor( when (NostrEventKind.valueOf(nostrEvent.kind)) { NostrEventKind.ZapRequest, NostrEventKind.WalletRequest -> { - handlePublishEvent(walletRelays, nostrEvent) + if (walletRelays.isEmpty()) { + throw MissingRelaysException() + } else { + handlePublishEvent(walletRelays, nostrEvent) + } } else -> { - handlePublishEvent(regularRelays, nostrEvent) + if (regularRelays.isEmpty()) { + throw MissingRelaysException() + } else { + handlePublishEvent(regularRelays, nostrEvent) + } } } } diff --git a/app/src/main/kotlin/net/primal/android/networking/relays/errors/MissingRelaysException.kt b/app/src/main/kotlin/net/primal/android/networking/relays/errors/MissingRelaysException.kt new file mode 100644 index 000000000..3704223e6 --- /dev/null +++ b/app/src/main/kotlin/net/primal/android/networking/relays/errors/MissingRelaysException.kt @@ -0,0 +1,3 @@ +package net.primal.android.networking.relays.errors + +class MissingRelaysException : RuntimeException() diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ProfileContract.kt b/app/src/main/kotlin/net/primal/android/profile/details/ProfileContract.kt index 1da2f602c..28f011714 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ProfileContract.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ProfileContract.kt @@ -26,6 +26,9 @@ interface ProfileContract { data class FailedToPublishZapEvent(val cause: Throwable) : ProfileError() data class FailedToPublishRepostEvent(val cause: Throwable) : ProfileError() data class FailedToPublishLikeEvent(val cause: Throwable) : ProfileError() + data class MissingRelaysConfiguration(val cause: Throwable) : ProfileError() + data class FailedToFollowProfile(val cause: Throwable) : ProfileError() + data class FailedToUnfollowProfile(val cause: Throwable) : ProfileError() } } diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ProfileScreen.kt b/app/src/main/kotlin/net/primal/android/profile/details/ProfileScreen.kt index db4cd6233..237921c12 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ProfileScreen.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ProfileScreen.kt @@ -736,6 +736,9 @@ private fun ErrorHandler( is ProfileError.FailedToPublishZapEvent -> context.getString(R.string.post_action_zap_failed) is ProfileError.FailedToPublishLikeEvent -> context.getString(R.string.post_action_like_failed) is ProfileError.FailedToPublishRepostEvent -> context.getString(R.string.post_action_repost_failed) + is ProfileError.FailedToFollowProfile -> context.getString(R.string.profile_error_unable_to_follow) + is ProfileError.FailedToUnfollowProfile -> context.getString(R.string.profile_error_unable_to_unfollow) + is ProfileError.MissingRelaysConfiguration -> context.getString(R.string.app_missing_relays_config) else -> return@LaunchedEffect } diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ProfileViewModel.kt b/app/src/main/kotlin/net/primal/android/profile/details/ProfileViewModel.kt index f980879ac..84fb38f94 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ProfileViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ProfileViewModel.kt @@ -18,6 +18,7 @@ import net.primal.android.core.compose.media.model.MediaResourceUi import net.primal.android.feed.repository.FeedRepository import net.primal.android.feed.repository.PostRepository import net.primal.android.navigation.profileId +import net.primal.android.networking.relays.errors.MissingRelaysException import net.primal.android.networking.relays.errors.NostrPublishException import net.primal.android.networking.sockets.errors.WssException import net.primal.android.profile.db.authorNameUiFriendly @@ -27,7 +28,6 @@ import net.primal.android.profile.details.ProfileContract.UiState import net.primal.android.profile.details.ProfileContract.UiState.ProfileError import net.primal.android.profile.details.model.ProfileDetailsUi import net.primal.android.profile.details.model.ProfileStatsUi -import net.primal.android.profile.repository.LatestFollowingResolver import net.primal.android.profile.repository.ProfileRepository import net.primal.android.user.accounts.active.ActiveAccountStore import net.primal.android.wallet.model.ZapTarget @@ -153,6 +153,8 @@ class ProfileViewModel @Inject constructor( ) } catch (error: NostrPublishException) { setErrorState(error = ProfileError.FailedToPublishLikeEvent(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = ProfileError.MissingRelaysConfiguration(error)) } } @@ -165,6 +167,8 @@ class ProfileViewModel @Inject constructor( ) } catch (error: NostrPublishException) { setErrorState(error = ProfileError.FailedToPublishRepostEvent(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = ProfileError.MissingRelaysConfiguration(error)) } } @@ -189,6 +193,8 @@ class ProfileViewModel @Inject constructor( setErrorState(error = ProfileError.FailedToPublishZapEvent(error)) } catch (error: NostrPublishException) { setErrorState(error = ProfileError.FailedToPublishZapEvent(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = ProfileError.MissingRelaysConfiguration(error)) } catch (error: ZapRepository.InvalidZapRequestException) { setErrorState(error = ProfileError.InvalidZapRequest(error)) } @@ -196,21 +202,31 @@ class ProfileViewModel @Inject constructor( private fun follow(followAction: UiEvent.FollowAction) = viewModelScope.launch { try { - profileRepository.follow(followAction.profileId) - } catch (error: LatestFollowingResolver.RemoteFollowingsUnavailableException) { - // Failed to retrieve latest contacts, propagate error to the UI + profileRepository.follow( + userId = activeAccountStore.activeUserId(), + followedUserId = followAction.profileId, + ) + } catch (error: WssException) { + setErrorState(error = ProfileError.FailedToFollowProfile(error)) } catch (error: NostrPublishException) { - // Failed to publish update, propagate error to the UI + setErrorState(error = ProfileError.FailedToFollowProfile(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = ProfileError.MissingRelaysConfiguration(error)) } } private fun unfollow(unfollowAction: UiEvent.UnfollowAction) = viewModelScope.launch { try { - profileRepository.unfollow(unfollowAction.profileId) - } catch (error: LatestFollowingResolver.RemoteFollowingsUnavailableException) { - // Failed to retrieve latest contacts, propagate error to the UI + profileRepository.unfollow( + userId = activeAccountStore.activeUserId(), + unfollowedUserId = unfollowAction.profileId, + ) + } catch (error: WssException) { + setErrorState(error = ProfileError.FailedToUnfollowProfile(error)) } catch (error: NostrPublishException) { - // Propagate error to the UI + setErrorState(error = ProfileError.FailedToUnfollowProfile(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = ProfileError.MissingRelaysConfiguration(error)) } } diff --git a/app/src/main/kotlin/net/primal/android/profile/repository/LatestFollowingResolver.kt b/app/src/main/kotlin/net/primal/android/profile/repository/LatestFollowingResolver.kt deleted file mode 100644 index 258ba89cc..000000000 --- a/app/src/main/kotlin/net/primal/android/profile/repository/LatestFollowingResolver.kt +++ /dev/null @@ -1,45 +0,0 @@ -package net.primal.android.profile.repository - -import net.primal.android.user.accounts.active.ActiveAccountStore -import net.primal.android.user.accounts.parseFollowings -import net.primal.android.user.api.UsersApi -import javax.inject.Inject - -class LatestFollowingResolver @Inject constructor( - private val usersApi: UsersApi, - private val activeAccountStore: ActiveAccountStore, -) { - - suspend fun getLatestFollowing(): Set { - val activeAccount = activeAccountStore.activeUserAccount() - val contactsResponse = usersApi.getUserContacts( - pubkey = activeAccount.pubkey, - extendedResponse = false, - ) - - if (contactsResponse.contactsEvent == null) { - throw RemoteFollowingsUnavailableException() - } - - return compareAndReturnLatestFollowing( - localCreatedAt = activeAccount.contactsCreatedAt, - localFollowings = activeAccount.following, - remoteCreatedAt = contactsResponse.contactsEvent.createdAt, - remoteFollowings = contactsResponse.contactsEvent.tags?.parseFollowings() ?: emptySet(), - ) - } - - private fun compareAndReturnLatestFollowing( - localCreatedAt: Long?, - localFollowings: Set, - remoteCreatedAt: Long, - remoteFollowings: Set, - ): Set = when { - localCreatedAt == null -> remoteFollowings - localCreatedAt >= remoteCreatedAt -> localFollowings - else -> remoteFollowings - } - - inner class RemoteFollowingsUnavailableException : RuntimeException() - -} diff --git a/app/src/main/kotlin/net/primal/android/profile/repository/ProfileRepository.kt b/app/src/main/kotlin/net/primal/android/profile/repository/ProfileRepository.kt index 5f83a8dd4..804b1c763 100644 --- a/app/src/main/kotlin/net/primal/android/profile/repository/ProfileRepository.kt +++ b/app/src/main/kotlin/net/primal/android/profile/repository/ProfileRepository.kt @@ -5,20 +5,21 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.withContext import net.primal.android.db.PrimalDatabase +import net.primal.android.networking.relays.errors.MissingRelaysException import net.primal.android.nostr.ext.asProfileMetadataPO import net.primal.android.nostr.ext.asProfileStats import net.primal.android.nostr.ext.takeContentAsUserProfileStatsOrNull -import net.primal.android.user.accounts.active.ActiveAccountStore +import net.primal.android.user.accounts.UserAccountFetcher import net.primal.android.user.api.UsersApi +import net.primal.android.user.domain.asUserAccount import net.primal.android.user.repository.UserRepository import javax.inject.Inject class ProfileRepository @Inject constructor( private val database: PrimalDatabase, private val usersApi: UsersApi, - private val activeAccountStore: ActiveAccountStore, private val userRepository: UserRepository, - private val latestFollowingResolver: LatestFollowingResolver, + private val userAccountFetcher: UserAccountFetcher, ) { fun observeProfile(profileId: String) = database.profiles().observeProfile(profileId = profileId).filterNotNull() @@ -43,35 +44,32 @@ class ProfileRepository @Inject constructor( } } - suspend fun follow(followedPubkey: String) { - updateFollowing( - newFollowing = latestFollowingResolver.getLatestFollowing() - .toMutableSet() - .apply { - add(followedPubkey) - } - ) + suspend fun follow(userId: String, followedUserId: String) { + updateFollowing(userId = userId) { + toMutableSet().apply { add(followedUserId) } + } } - suspend fun unfollow(unfollowedPubkey: String) { - updateFollowing( - newFollowing = latestFollowingResolver.getLatestFollowing() - .toMutableSet() - .apply { remove(unfollowedPubkey) } - ) + suspend fun unfollow(userId: String, unfollowedUserId: String) { + updateFollowing(userId = userId) { + toMutableSet().apply { remove(unfollowedUserId) } + } } - private suspend fun updateFollowing(newFollowing: Set) { - val activeAccount = activeAccountStore.activeUserAccount() - val newContactsNostrEvent = usersApi.setUserContacts( - ownerId = activeAccount.pubkey, - contacts = newFollowing, - relays = activeAccount.relays - ) + private suspend fun updateFollowing( + userId: String, + reducer: Set.() -> Set + ) { + val userContacts = userAccountFetcher.fetchUserContacts(pubkey = userId) + ?: throw MissingRelaysException() - userRepository.updateContacts( - userId = activeAccount.pubkey, - contactsNostrEvent = newContactsNostrEvent + userRepository.updateContacts(userId, userContacts) + + val newContactsNostrEvent = usersApi.setUserContacts( + ownerId = userId, + contacts = userContacts.following.reducer(), + relays = userContacts.relays, ) + userRepository.updateContacts(userId, newContactsNostrEvent.asUserAccount()) } } diff --git a/app/src/main/kotlin/net/primal/android/settings/repository/SettingsRepository.kt b/app/src/main/kotlin/net/primal/android/settings/repository/SettingsRepository.kt index de90f8f9e..c1c0ee0d8 100644 --- a/app/src/main/kotlin/net/primal/android/settings/repository/SettingsRepository.kt +++ b/app/src/main/kotlin/net/primal/android/settings/repository/SettingsRepository.kt @@ -57,7 +57,7 @@ class SettingsRepository @Inject constructor( } private suspend fun persistAppSettings(userId: String, appSettings: ContentAppSettings) { - val currentUserAccount = accountsStore.findByIdOrNull(pubkey = userId) + val currentUserAccount = accountsStore.findByIdOrNull(userId = userId) ?: UserAccount.buildLocal(pubkey = userId) accountsStore.upsertAccount( diff --git a/app/src/main/kotlin/net/primal/android/thread/ThreadContract.kt b/app/src/main/kotlin/net/primal/android/thread/ThreadContract.kt index 3fad9f835..d433f7747 100644 --- a/app/src/main/kotlin/net/primal/android/thread/ThreadContract.kt +++ b/app/src/main/kotlin/net/primal/android/thread/ThreadContract.kt @@ -22,6 +22,7 @@ interface ThreadContract { data class FailedToPublishRepostEvent(val cause: Throwable) : ThreadError() data class FailedToPublishReplyEvent(val cause: Throwable) : ThreadError() data class FailedToPublishLikeEvent(val cause: Throwable) : ThreadError() + data class MissingRelaysConfiguration(val cause: Throwable) : ThreadError() } } diff --git a/app/src/main/kotlin/net/primal/android/thread/ThreadScreen.kt b/app/src/main/kotlin/net/primal/android/thread/ThreadScreen.kt index a09dfb5cc..bae3a91b8 100644 --- a/app/src/main/kotlin/net/primal/android/thread/ThreadScreen.kt +++ b/app/src/main/kotlin/net/primal/android/thread/ThreadScreen.kt @@ -417,6 +417,7 @@ private fun ErrorHandler( is ThreadError.FailedToPublishLikeEvent -> context.getString(R.string.post_action_like_failed) is ThreadError.FailedToPublishRepostEvent -> context.getString(R.string.post_action_repost_failed) is ThreadError.FailedToPublishReplyEvent -> context.getString(R.string.post_action_reply_failed) + is ThreadError.MissingRelaysConfiguration -> context.getString(R.string.app_missing_relays_config) null -> return@LaunchedEffect } diff --git a/app/src/main/kotlin/net/primal/android/thread/ThreadViewModel.kt b/app/src/main/kotlin/net/primal/android/thread/ThreadViewModel.kt index 6080e4401..988f1632d 100644 --- a/app/src/main/kotlin/net/primal/android/thread/ThreadViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/thread/ThreadViewModel.kt @@ -19,6 +19,7 @@ import net.primal.android.core.compose.feed.asFeedPostUi import net.primal.android.feed.repository.FeedRepository import net.primal.android.feed.repository.PostRepository import net.primal.android.navigation.postId +import net.primal.android.networking.relays.errors.MissingRelaysException import net.primal.android.networking.relays.errors.NostrPublishException import net.primal.android.networking.sockets.errors.WssException import net.primal.android.nostr.ext.asEventIdTag @@ -146,6 +147,8 @@ class ThreadViewModel @Inject constructor( ) } catch (error: NostrPublishException) { setErrorState(error = ThreadError.FailedToPublishLikeEvent(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = ThreadError.MissingRelaysConfiguration(error)) } } @@ -158,6 +161,8 @@ class ThreadViewModel @Inject constructor( ) } catch (error: NostrPublishException) { setErrorState(error = ThreadError.FailedToPublishRepostEvent(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = ThreadError.MissingRelaysConfiguration(error)) } } @@ -182,6 +187,8 @@ class ThreadViewModel @Inject constructor( setErrorState(error = ThreadError.FailedToPublishZapEvent(error)) } catch (error: NostrPublishException) { setErrorState(error = ThreadError.FailedToPublishZapEvent(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = ThreadError.MissingRelaysConfiguration(error)) } catch (error: ZapRepository.InvalidZapRequestException) { setErrorState(error = ThreadError.InvalidZapRequest(error)) } @@ -227,6 +234,8 @@ class ThreadViewModel @Inject constructor( setState { copy(replyText = "") } } catch (error: NostrPublishException) { setErrorState(error = ThreadError.FailedToPublishReplyEvent(error)) + } catch (error: MissingRelaysException) { + setErrorState(error = ThreadError.MissingRelaysConfiguration(error)) } finally { setState { copy(publishingReply = false) } } diff --git a/app/src/main/kotlin/net/primal/android/user/accounts/BootstrapRelays.kt b/app/src/main/kotlin/net/primal/android/user/accounts/BootstrapRelays.kt index f467a7694..5dbe4fa37 100644 --- a/app/src/main/kotlin/net/primal/android/user/accounts/BootstrapRelays.kt +++ b/app/src/main/kotlin/net/primal/android/user/accounts/BootstrapRelays.kt @@ -13,4 +13,3 @@ val BOOTSTRAP_RELAYS = listOf( "wss://nostr.bitcoiner.social", "wss://relay.primal.net", ) - diff --git a/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountFetcher.kt b/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountFetcher.kt index c1071e57d..3a0f4261b 100644 --- a/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountFetcher.kt +++ b/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountFetcher.kt @@ -3,13 +3,11 @@ package net.primal.android.user.accounts import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import net.primal.android.core.utils.asEllipsizedNpub -import net.primal.android.networking.sockets.errors.WssException import net.primal.android.nostr.ext.asProfileMetadataPO import net.primal.android.nostr.ext.takeContentAsUserProfileStatsOrNull import net.primal.android.profile.db.authorNameUiFriendly import net.primal.android.profile.db.userNameUiFriendly import net.primal.android.user.api.UsersApi -import net.primal.android.user.domain.Relay import net.primal.android.user.domain.UserAccount import net.primal.android.user.domain.asUserAccount import javax.inject.Inject @@ -38,44 +36,11 @@ class UserAccountFetcher @Inject constructor( ) } - suspend fun fetchUserProfileOrNull(pubkey: String) = try { - fetchUserProfile(pubkey) - } catch (error: WssException) { - null - } - - suspend fun fetchUserContacts(pubkey: String): UserAccount { + suspend fun fetchUserContacts(pubkey: String): UserAccount? { val contactsResponse = withContext(Dispatchers.IO) { usersApi.getUserContacts(pubkey = pubkey, extendedResponse = false) } - val userAccount = contactsResponse.contactsEvent?.asUserAccount() - ?: UserAccount( - pubkey = pubkey, - authorDisplayName = pubkey.asEllipsizedNpub(), - userDisplayName = pubkey.asEllipsizedNpub(), - relays = BOOTSTRAP_RELAYS.map { Relay(url = it, read = true, write = true) }, - following = emptySet(), - interests = emptyList(), - contactsCreatedAt = null, - ) - - return userAccount.ensureRelaysAreAvailable() - } - - suspend fun fetchUserContactsOrNull(pubkey: String) = try { - fetchUserContacts(pubkey) - } catch (error: WssException) { - null - } - - private fun UserAccount.ensureRelaysAreAvailable(): UserAccount { - return copy( - relays = if (relays.isEmpty()) { - BOOTSTRAP_RELAYS.map { Relay(url = it, read = true, write = true) } - } else { - this.relays - } - ) + return contactsResponse.contactsEvent?.asUserAccount() } } diff --git a/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountsStore.kt b/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountsStore.kt index aeb163f36..adb5d1504 100644 --- a/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountsStore.kt +++ b/app/src/main/kotlin/net/primal/android/user/accounts/UserAccountsStore.kt @@ -24,6 +24,16 @@ class UserAccountsStore @Inject constructor( initialValue = runBlocking { persistence.data.first() }, ) + suspend fun getAndUpdateAccount( + userId: String, + reducer: UserAccount.() -> UserAccount, + ): UserAccount { + val current = findByIdOrNull(userId = userId) ?: UserAccount.buildLocal(pubkey = userId) + val updated = current.reducer() + upsertAccount(updated) + return updated + } + suspend fun upsertAccount(userAccount: UserAccount) { persistence.updateData { accounts -> val existingIndex = accounts.indexOfFirst { it.pubkey == userAccount.pubkey } @@ -45,5 +55,5 @@ class UserAccountsStore @Inject constructor( persistence.updateData { emptyList() } } - fun findByIdOrNull(pubkey: String) = userAccounts.value.find { it.pubkey == pubkey } + fun findByIdOrNull(userId: String) = userAccounts.value.find { it.pubkey == userId } } diff --git a/app/src/main/kotlin/net/primal/android/user/domain/UserAccount.kt b/app/src/main/kotlin/net/primal/android/user/domain/UserAccount.kt index 47c20a17a..d85929199 100644 --- a/app/src/main/kotlin/net/primal/android/user/domain/UserAccount.kt +++ b/app/src/main/kotlin/net/primal/android/user/domain/UserAccount.kt @@ -15,7 +15,6 @@ data class UserAccount( val followingCount: Int? = null, val followersCount: Int? = null, val notesCount: Int? = null, - val contactsCreatedAt: Long? = null, val nostrWallet: NostrWallet? = null, val appSettings: ContentAppSettings? = null, val relays: List = emptyList(), diff --git a/app/src/main/kotlin/net/primal/android/user/domain/UserAccountExt.kt b/app/src/main/kotlin/net/primal/android/user/domain/UserAccountExt.kt index 4ddcd2612..76b04ba8e 100644 --- a/app/src/main/kotlin/net/primal/android/user/domain/UserAccountExt.kt +++ b/app/src/main/kotlin/net/primal/android/user/domain/UserAccountExt.kt @@ -13,5 +13,4 @@ fun NostrEvent.asUserAccount() = UserAccount( relays = content.parseRelays(), following = tags?.parseFollowings() ?: emptySet(), interests = tags?.parseInterests() ?: emptyList(), - contactsCreatedAt = createdAt, ) diff --git a/app/src/main/kotlin/net/primal/android/user/repository/UserRepository.kt b/app/src/main/kotlin/net/primal/android/user/repository/UserRepository.kt index dd1cc9f63..c564a897d 100644 --- a/app/src/main/kotlin/net/primal/android/user/repository/UserRepository.kt +++ b/app/src/main/kotlin/net/primal/android/user/repository/UserRepository.kt @@ -1,14 +1,11 @@ package net.primal.android.user.repository -import net.primal.android.nostr.ext.isPubKeyTag -import net.primal.android.nostr.model.NostrEvent import net.primal.android.user.accounts.UserAccountFetcher import net.primal.android.user.accounts.UserAccountsStore import net.primal.android.user.accounts.copyContactsIfNotNull import net.primal.android.user.accounts.copyIfNotNull import net.primal.android.user.domain.NostrWallet import net.primal.android.user.domain.UserAccount -import net.primal.android.user.domain.asUserAccount import javax.inject.Inject class UserRepository @Inject constructor( @@ -16,54 +13,40 @@ class UserRepository @Inject constructor( private val accountsStore: UserAccountsStore, ) { - suspend fun fetchAndUpsertUserAccount(userId: String) { - val userProfile = userAccountFetcher.fetchUserProfileOrNull(pubkey = userId) - val userContacts = userAccountFetcher.fetchUserContactsOrNull(pubkey = userId) - - val currentUserAccount = accountsStore.findByIdOrNull(pubkey = userId) - ?: UserAccount.buildLocal(pubkey = userId) + suspend fun createNewUserAccount(userId: String): UserAccount { + val account = UserAccount.buildLocal(pubkey = userId) + accountsStore.upsertAccount(account) + return account + } - accountsStore.upsertAccount( - userAccount = currentUserAccount.copyIfNotNull( + suspend fun fetchAndUpdateUserAccount(userId: String): UserAccount { + val userProfile = userAccountFetcher.fetchUserProfile(pubkey = userId) + val userContacts = userAccountFetcher.fetchUserContacts(pubkey = userId) + return accountsStore.getAndUpdateAccount(userId = userId) { + copyIfNotNull( profile = userProfile, contacts = userContacts, ) - ) + } } - suspend fun updateContacts(userId: String, contactsNostrEvent: NostrEvent) { - val currentUserAccount = accountsStore.findByIdOrNull(pubkey = userId) - ?: UserAccount.buildLocal(pubkey = userId) - - accountsStore.upsertAccount( - userAccount = currentUserAccount.copyContactsIfNotNull( - contacts = contactsNostrEvent.asUserAccount() - ).copy( - followingCount = contactsNostrEvent.tags?.count { it.isPubKeyTag() } - ) - ) + suspend fun updateContacts(userId: String, contactsUserAccount: UserAccount) { + accountsStore.getAndUpdateAccount(userId = userId) { + copyContactsIfNotNull(contacts = contactsUserAccount) + .copy(followingCount = contactsUserAccount.following.size) + } } suspend fun connectNostrWallet(userId: String, nostrWalletConnect: NostrWallet) { - val currentUserAccount = accountsStore.findByIdOrNull(pubkey = userId) - ?: UserAccount.buildLocal(pubkey = userId) - - accountsStore.upsertAccount( - userAccount = currentUserAccount.copy( - nostrWallet = nostrWalletConnect - ) - ) + accountsStore.getAndUpdateAccount(userId = userId) { + copy(nostrWallet = nostrWalletConnect) + } } suspend fun disconnectNostrWallet(userId: String) { - val currentUserAccount = accountsStore.findByIdOrNull(pubkey = userId) - ?: UserAccount.buildLocal(pubkey = userId) - - accountsStore.upsertAccount( - userAccount = currentUserAccount.copy( - nostrWallet = null - ) - ) + accountsStore.getAndUpdateAccount(userId = userId) { + copy(nostrWallet = null) + } } suspend fun removeAllUserAccounts() { diff --git a/app/src/main/kotlin/net/primal/android/user/updater/UserDataUpdater.kt b/app/src/main/kotlin/net/primal/android/user/updater/UserDataUpdater.kt index 264daed73..e0e2582ef 100644 --- a/app/src/main/kotlin/net/primal/android/user/updater/UserDataUpdater.kt +++ b/app/src/main/kotlin/net/primal/android/user/updater/UserDataUpdater.kt @@ -33,6 +33,6 @@ class UserDataUpdater @AssistedInject constructor( private suspend fun updateData() { settingsRepository.fetchAndPersistAppSettings(userId = userId) - userRepository.fetchAndUpsertUserAccount(userId = userId) + userRepository.fetchAndUpdateUserAccount(userId = userId) } } diff --git a/app/src/main/kotlin/net/primal/android/wallet/repository/ZapRepository.kt b/app/src/main/kotlin/net/primal/android/wallet/repository/ZapRepository.kt index faad23df0..ff78405d4 100644 --- a/app/src/main/kotlin/net/primal/android/wallet/repository/ZapRepository.kt +++ b/app/src/main/kotlin/net/primal/android/wallet/repository/ZapRepository.kt @@ -27,7 +27,7 @@ class ZapRepository @Inject constructor( amountInSats: ULong? = null, comment: String? = null, ) { - val userAccount = accountsStore.findByIdOrNull(pubkey = userId) + val userAccount = accountsStore.findByIdOrNull(userId = userId) val nostrWallet = userAccount?.nostrWallet val walletRelays = userAccount?.relays val defaultZapAmount = userAccount?.appSettings?.defaultZapAmount diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4355e9b50..7f32ac842 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ npub Something went wrong. Please try again. + No relays found. Please configure your Network. Sign in Already have a Nostr account? Sign with your Nostr key. @@ -128,6 +129,8 @@ posts follow unfollow + Unable to follow profile. Please try again. + Unable to unfollow profile. Please try again. Add a comment… Zap diff --git a/app/src/test/kotlin/net/primal/android/profile/repository/LatestFollowingResolverTest.kt b/app/src/test/kotlin/net/primal/android/profile/repository/LatestFollowingResolverTest.kt deleted file mode 100644 index 3895f2857..000000000 --- a/app/src/test/kotlin/net/primal/android/profile/repository/LatestFollowingResolverTest.kt +++ /dev/null @@ -1,128 +0,0 @@ -package net.primal.android.profile.repository - -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.matchers.collections.shouldContain -import io.kotest.matchers.shouldBe -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.runTest -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.add -import kotlinx.serialization.json.buildJsonArray -import net.primal.android.nostr.model.NostrEvent -import net.primal.android.nostr.model.NostrEventKind -import net.primal.android.user.accounts.active.ActiveAccountStore -import net.primal.android.user.api.UsersApi -import net.primal.android.user.api.model.UserContactsResponse -import net.primal.android.user.domain.UserAccount -import org.junit.Test -import java.time.Instant - -class LatestFollowingResolverTest { - - private fun buildLatestFollowingResolver( - usersApi: UsersApi = mockk(relaxed = true), - activeAccountStore: ActiveAccountStore = mockk(relaxed = true) { - every { activeUserAccount } returns MutableStateFlow(UserAccount.EMPTY) - } - ) = LatestFollowingResolver( - usersApi = usersApi, - activeAccountStore = activeAccountStore - ) - - private fun buildNostrContactsEvent( - pubKey: String = "test-pubkey", - content: String = "", - createdAt: Long = 0, - id: String = "test-id", - kind: Int = NostrEventKind.Contacts.value, - sig: String = "test-sig", - tags: List? = listOf() - ) = NostrEvent( - pubKey = pubKey, - content = content, - createdAt = createdAt, - id = id, - kind = kind, - sig = sig, - tags = tags - ) - - @Test - fun `getLatestFollowing throws exception for missing contacts event`() = runTest { - val resolver = buildLatestFollowingResolver( - usersApi = mockk { - coEvery { getUserContacts(any(), any()) } returns UserContactsResponse() - }, - ) - - shouldThrow { - resolver.getLatestFollowing() - } - } - - @Test - fun `getLatestFollowing returns remote contacts when newer available`() = runTest { - val resolver = buildLatestFollowingResolver( - usersApi = mockk { - coEvery { getUserContacts(any(), any()) } returns UserContactsResponse( - contactsEvent = buildNostrContactsEvent( - createdAt = Instant.now().epochSecond, - tags = listOf( - buildJsonArray { - add("p") - add("pubkey1") - }, - buildJsonArray { - add("p") - add("pubkey2") - }, - ), - ) - ) - }, - ) - - val actual = resolver.getLatestFollowing() - actual.size shouldBe 2 - actual.shouldContain("pubkey1") - actual.shouldContain("pubkey2") - } - - @Test - fun `getLatestFollowing returns local contacts when no newer available`() = runTest { - val resolver = buildLatestFollowingResolver( - activeAccountStore = mockk { - coEvery { activeUserAccount() } returns UserAccount.EMPTY.copy( - contactsCreatedAt = Instant.now().epochSecond, - following = setOf("pubkey10", "pubkey11") - ) - }, - usersApi = mockk { - coEvery { getUserContacts(any(), any()) } returns UserContactsResponse( - contactsEvent = buildNostrContactsEvent( - createdAt = 0, - tags = listOf( - buildJsonArray { - add("p") - add("pubkey1") - }, - buildJsonArray { - add("p") - add("pubkey2") - }, - ), - ) - ) - }, - ) - - val actual = resolver.getLatestFollowing() - - actual.size shouldBe 2 - actual.shouldContain("pubkey10") - actual.shouldContain("pubkey11") - } -} diff --git a/app/src/test/kotlin/net/primal/android/user/accounts/UserAccountFetcherTest.kt b/app/src/test/kotlin/net/primal/android/user/accounts/UserAccountFetcherTest.kt index 17ab9b338..d3cbd05c7 100644 --- a/app/src/test/kotlin/net/primal/android/user/accounts/UserAccountFetcherTest.kt +++ b/app/src/test/kotlin/net/primal/android/user/accounts/UserAccountFetcherTest.kt @@ -134,35 +134,12 @@ class UserAccountFetcherTest { val fetcher = UserAccountFetcher(usersApi = usersApiMock) val actual = fetcher.fetchUserContacts(pubkey = expectedPubkey) + actual.shouldNotBeNull() actual.relays shouldBe expectedRelays actual.following shouldBe expectedFollowing actual.interests shouldBe listOf("#bitcoin") } - @Test - fun `fetchUserContacts takes bootstrap relays if relays are missing`() = runTest { - val expectedPubkey = "9b46c3f4a8dcdafdfff12a97c59758f38ff55002370fcfa7d14c8c857e9b5812" - val usersApiMock = mockk { - coEvery { getUserContacts(any(), any()) } returns UserContactsResponse( - contactsEvent = NostrEvent( - id = "invalidId", - pubKey = expectedPubkey, - createdAt = 1683463925, - kind = 3, - tags = emptyList(), - content = "", - sig = "invalidSig" - ), - ) - } - - val fetcher = UserAccountFetcher(usersApi = usersApiMock) - val actual = fetcher.fetchUserContacts(pubkey = expectedPubkey).relays - - actual.shouldNotBeNull() - actual.map { it.url } shouldBe BOOTSTRAP_RELAYS - } - @Test fun `fetchUserContacts fails if api call fails`() = runTest { val usersApiMock = mockk { diff --git a/app/src/test/kotlin/net/primal/android/user/accounts/UserAccountsStoreTest.kt b/app/src/test/kotlin/net/primal/android/user/accounts/UserAccountsStoreTest.kt index ca33b9c8f..7fc7a11fd 100644 --- a/app/src/test/kotlin/net/primal/android/user/accounts/UserAccountsStoreTest.kt +++ b/app/src/test/kotlin/net/primal/android/user/accounts/UserAccountsStoreTest.kt @@ -124,7 +124,7 @@ class UserAccountsStoreTest { persistence.updateData { it.toMutableList().apply { add((existingAccount)) } } val accountsStore = UserAccountsStore(persistence) - val actual = accountsStore.findByIdOrNull(pubkey = expectedPubkey) + val actual = accountsStore.findByIdOrNull(userId = expectedPubkey) actual.shouldNotBeNull() actual shouldBe existingAccount } @@ -132,7 +132,7 @@ class UserAccountsStoreTest { @Test fun `findByIdOrNull returns null for not found id`() { val accountsStore = UserAccountsStore(persistence) - val actual = accountsStore.findByIdOrNull(pubkey = "nonExisting") + val actual = accountsStore.findByIdOrNull(userId = "nonExisting") actual.shouldBeNull() }