From 8e1bae24f57ef772d7d90617987b1d495598651f Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Thu, 28 Nov 2024 16:12:11 +0100 Subject: [PATCH 1/7] Implement follow approvals --- .../explore/home/people/ExplorePeople.kt | 62 ++++++++++++++- .../home/people/ExplorePeopleContract.kt | 13 +++- .../home/people/ExplorePeopleViewModel.kt | 24 +++++- .../profile/details/ProfileDetailsContract.kt | 14 +++- .../details/ProfileDetailsViewModel.kt | 12 +++ ...ConfirmFollowUnfollowProfileAlertDialog.kt | 75 +++++++++++++++++++ .../details/ui/ProfileDetailsHeader.kt | 18 ++++- .../details/ui/ProfileDetailsScreen.kt | 28 +++++++ .../profile/follows/ProfileFollowsContract.kt | 15 +++- .../profile/follows/ProfileFollowsScreen.kt | 73 +++++++++++++++++- .../follows/ProfileFollowsViewModel.kt | 20 ++++- .../profile/repository/ProfileRepository.kt | 52 ++++++++----- .../details/ArticleDetailsViewModel.kt | 2 + app/src/main/res/values/strings.xml | 10 +++ 14 files changed, 379 insertions(+), 39 deletions(-) create mode 100644 app/src/main/kotlin/net/primal/android/profile/details/ui/ConfirmFollowUnfollowProfileAlertDialog.kt diff --git a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt index cbab29990..886ef9ec3 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt @@ -21,6 +21,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -46,6 +49,8 @@ import net.primal.android.core.compose.profile.model.ProfileDetailsUi import net.primal.android.core.errors.UiError import net.primal.android.core.utils.shortened import net.primal.android.explore.api.model.ExplorePeopleData +import net.primal.android.profile.details.ui.ConfirmFollowUnfollowProfileAlertDialog +import net.primal.android.profile.details.ui.ProfileAction import net.primal.android.theme.AppTheme import net.primal.android.theme.domain.PrimalTheme @@ -81,6 +86,14 @@ fun ExplorePeople( eventPublisher: (ExplorePeopleContract.UiEvent) -> Unit, onProfileClick: (String) -> Unit, ) { + var lastFollowUnfollowProfileId by rememberSaveable { mutableStateOf(null) } + + ApprovalAlertDialogs( + state = state, + eventPublisher = eventPublisher, + lastFollowUnfollowProfileId = lastFollowUnfollowProfileId, + ) + if (state.loading && state.people.isEmpty()) { HeightAdjustableLoadingLazyListPlaceholder( modifier = modifier.fillMaxSize(), @@ -116,13 +129,18 @@ fun ExplorePeople( isFollowed = state.userFollowing.contains(item.profile.pubkey), onItemClick = { onProfileClick(item.profile.pubkey) }, onFollowClick = { + lastFollowUnfollowProfileId = item.profile.pubkey eventPublisher( - ExplorePeopleContract.UiEvent.FollowUser(item.profile.pubkey), + ExplorePeopleContract.UiEvent.FollowUser(userId = item.profile.pubkey, forceUpdate = false), ) }, onUnfollowClick = { + lastFollowUnfollowProfileId = item.profile.pubkey eventPublisher( - ExplorePeopleContract.UiEvent.UnfollowUser(item.profile.pubkey), + ExplorePeopleContract.UiEvent.UnfollowUser( + userId = item.profile.pubkey, + forceUpdate = false, + ), ) }, ) @@ -133,6 +151,46 @@ fun ExplorePeople( } } +@Composable +private fun ApprovalAlertDialogs( + state: ExplorePeopleContract.UiState, + eventPublisher: (ExplorePeopleContract.UiEvent) -> Unit, + lastFollowUnfollowProfileId: String?, +) { + if (state.shouldApproveFollow) { + ConfirmFollowUnfollowProfileAlertDialog( + onClose = { eventPublisher(ExplorePeopleContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, + onActionConfirmed = { + lastFollowUnfollowProfileId?.let { + eventPublisher( + ExplorePeopleContract.UiEvent.FollowUser( + userId = it, + forceUpdate = true, + ), + ) + } + }, + profileAction = ProfileAction.Follow, + ) + } + if (state.shouldApproveUnfollow) { + ConfirmFollowUnfollowProfileAlertDialog( + onClose = { eventPublisher(ExplorePeopleContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, + onActionConfirmed = { + lastFollowUnfollowProfileId?.let { + eventPublisher( + ExplorePeopleContract.UiEvent.UnfollowUser( + userId = it, + forceUpdate = true, + ), + ) + } + }, + profileAction = ProfileAction.Unfollow, + ) + } +} + @Composable private fun ExplorePersonListItem( modifier: Modifier = Modifier, diff --git a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleContract.kt b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleContract.kt index 01d28ee77..eb9608ffc 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleContract.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleContract.kt @@ -10,12 +10,21 @@ interface ExplorePeopleContract { val people: List = emptyList(), val userFollowing: Set = emptySet(), val error: UiError? = null, + val shouldApproveFollow: Boolean = false, + val shouldApproveUnfollow: Boolean = false, ) sealed class UiEvent { - data class FollowUser(val userId: String) : UiEvent() - data class UnfollowUser(val userId: String) : UiEvent() + data class FollowUser( + val userId: String, + val forceUpdate: Boolean, + ) : UiEvent() + data class UnfollowUser( + val userId: String, + val forceUpdate: Boolean, + ) : UiEvent() + data object DismissConfirmFollowUnfollowAlertDialog : UiEvent() data object RefreshPeople : UiEvent() data object DismissError : UiEvent() } diff --git a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt index f9ecc45ec..bf138f40e 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt @@ -76,15 +76,17 @@ class ExplorePeopleViewModel @Inject constructor( viewModelScope.launch { events.collect { when (it) { - is UiEvent.FollowUser -> follow(profileId = it.userId) - is UiEvent.UnfollowUser -> unfollow(profileId = it.userId) + is UiEvent.FollowUser -> follow(profileId = it.userId, forceUpdate = it.forceUpdate) + is UiEvent.UnfollowUser -> unfollow(profileId = it.userId, forceUpdate = it.forceUpdate) UiEvent.RefreshPeople -> fetchExplorePeople() UiEvent.DismissError -> setState { copy(error = null) } + UiEvent.DismissConfirmFollowUnfollowAlertDialog -> + setState { copy(shouldApproveFollow = false, shouldApproveUnfollow = false) } } } } - private fun follow(profileId: String) = + private fun follow(profileId: String, forceUpdate: Boolean) = viewModelScope.launch { updateStateProfileFollow(profileId) @@ -92,6 +94,7 @@ class ExplorePeopleViewModel @Inject constructor( profileRepository.follow( userId = activeAccountStore.activeUserId(), followedUserId = profileId, + forceUpdate = forceUpdate, ) } @@ -102,6 +105,12 @@ class ExplorePeopleViewModel @Inject constructor( is WssException, is NostrPublishException, is ProfileRepository.FollowListNotFound -> setState { copy(error = UiError.FailedToFollowUser(error)) } + is ProfileRepository.PossibleFollowListCorruption -> setState { + copy( + shouldApproveFollow = true, + ) + } + is MissingRelaysException -> setState { copy( error = UiError.MissingRelaysConfiguration(error), @@ -115,7 +124,7 @@ class ExplorePeopleViewModel @Inject constructor( } } - private fun unfollow(profileId: String) = + private fun unfollow(profileId: String, forceUpdate: Boolean) = viewModelScope.launch { updateStateProfileUnfollow(profileId) @@ -123,6 +132,7 @@ class ExplorePeopleViewModel @Inject constructor( profileRepository.unfollow( userId = activeAccountStore.activeUserId(), unfollowedUserId = profileId, + forceUpdate = forceUpdate, ) } @@ -133,6 +143,12 @@ class ExplorePeopleViewModel @Inject constructor( is WssException, is NostrPublishException, is ProfileRepository.FollowListNotFound -> setState { copy(error = UiError.FailedToUnfollowUser(error)) } + is ProfileRepository.PossibleFollowListCorruption -> setState { + copy( + shouldApproveUnfollow = true, + ) + } + is MissingRelaysException -> setState { copy( error = UiError.MissingRelaysConfiguration(error), diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsContract.kt b/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsContract.kt index 38c420471..6dca7d7bc 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsContract.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsContract.kt @@ -26,6 +26,8 @@ interface ProfileDetailsContract { ProfileFeedSpec.AuthoredMedia, ), val error: ProfileError? = null, + val shouldApproveFollow: Boolean = false, + val shouldApproveUnfollow: Boolean = false, val zapError: UiError? = null, val zappingState: ZappingState = ZappingState(), ) { @@ -48,14 +50,19 @@ interface ProfileDetailsContract { } sealed class UiEvent { - data class FollowAction(val profileId: String) : UiEvent() - data class UnfollowAction(val profileId: String) : UiEvent() + data class FollowAction( + val profileId: String, + val forceUpdate: Boolean, + ) : UiEvent() + data class UnfollowAction( + val profileId: String, + val forceUpdate: Boolean, + ) : UiEvent() data class AddProfileFeedAction( val profileId: String, val feedTitle: String, val feedDescription: String, ) : UiEvent() - data class ZapProfile( val profileId: String, val profileLnUrlDecoded: String?, @@ -70,5 +77,6 @@ interface ProfileDetailsContract { data class ReportAbuse(val type: ReportType, val profileId: String, val noteId: String? = null) : UiEvent() data object DismissError : UiEvent() data object DismissZapError : UiEvent() + data object DismissConfirmFollowUnfollowAlertDialog : UiEvent() } } diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt b/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt index b43396620..41cdf020c 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt @@ -120,6 +120,8 @@ class ProfileDetailsViewModel @Inject constructor( ) UiEvent.DismissZapError -> setState { copy(zapError = null) } + UiEvent.DismissConfirmFollowUnfollowAlertDialog -> + setState { copy(shouldApproveFollow = false, shouldApproveUnfollow = false) } } } } @@ -311,6 +313,7 @@ class ProfileDetailsViewModel @Inject constructor( profileRepository.follow( userId = activeAccountStore.activeUserId(), followedUserId = followAction.profileId, + forceUpdate = followAction.forceUpdate, ) } catch (error: WssException) { Timber.w(error) @@ -328,6 +331,10 @@ class ProfileDetailsViewModel @Inject constructor( Timber.w(error) updateStateProfileAsUnfollowed() setErrorState(error = ProfileError.FailedToFollowProfile(error)) + } catch (error: ProfileRepository.PossibleFollowListCorruption) { + Timber.w(error) + updateStateProfileAsUnfollowed() + setState { copy(shouldApproveFollow = true) } } } @@ -338,6 +345,7 @@ class ProfileDetailsViewModel @Inject constructor( profileRepository.unfollow( userId = activeAccountStore.activeUserId(), unfollowedUserId = unfollowAction.profileId, + forceUpdate = unfollowAction.forceUpdate, ) } catch (error: WssException) { Timber.w(error) @@ -355,6 +363,10 @@ class ProfileDetailsViewModel @Inject constructor( Timber.w(error) updateStateProfileAsFollowed() setErrorState(error = ProfileError.FailedToUnfollowProfile(error)) + } catch (error: ProfileRepository.PossibleFollowListCorruption) { + Timber.w(error) + updateStateProfileAsFollowed() + setState { copy(shouldApproveUnfollow = true) } } } diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ui/ConfirmFollowUnfollowProfileAlertDialog.kt b/app/src/main/kotlin/net/primal/android/profile/details/ui/ConfirmFollowUnfollowProfileAlertDialog.kt new file mode 100644 index 000000000..4481b3881 --- /dev/null +++ b/app/src/main/kotlin/net/primal/android/profile/details/ui/ConfirmFollowUnfollowProfileAlertDialog.kt @@ -0,0 +1,75 @@ +package net.primal.android.profile.details.ui + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import net.primal.android.R +import net.primal.android.theme.AppTheme + +@Composable +fun ConfirmFollowUnfollowProfileAlertDialog( + onClose: () -> Unit, + onActionConfirmed: () -> Unit, + profileAction: ProfileAction, +) { + val messages = when (profileAction) { + ProfileAction.Follow -> { + ApprovalMessages( + title = stringResource(id = R.string.context_confirm_follow_title), + text = stringResource(id = R.string.context_confirm_follow_text), + positive = stringResource(id = R.string.context_confirm_follow_positive), + negative = stringResource(id = R.string.context_confirm_follow_negative), + ) + } + ProfileAction.Unfollow -> { + ApprovalMessages( + title = stringResource(id = R.string.context_confirm_unfollow_title), + text = stringResource(id = R.string.context_confirm_unfollow_text), + positive = stringResource(id = R.string.context_confirm_unfollow_positive), + negative = stringResource(id = R.string.context_confirm_unfollow_negative), + ) + } + } + AlertDialog( + containerColor = AppTheme.colorScheme.surfaceVariant, + onDismissRequest = onClose, + title = { + Text( + text = messages.title, + style = AppTheme.typography.titleLarge, + ) + }, + text = { + Text( + text = messages.text, + style = AppTheme.typography.bodyLarge, + ) + }, + dismissButton = { + TextButton(onClick = onClose) { + Text(text = messages.negative) + } + }, + confirmButton = { + TextButton(onClick = onActionConfirmed) { + Text( + text = messages.positive, + ) + } + }, + ) +} + +private data class ApprovalMessages( + val title: String, + val text: String, + val positive: String, + val negative: String, +) + +enum class ProfileAction { + Follow, + Unfollow, +} diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsHeader.kt b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsHeader.kt index 751870e77..db60d283f 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsHeader.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsHeader.kt @@ -73,8 +73,22 @@ fun ProfileDetailsHeader( onUnableToZapProfile() } }, - onFollow = { eventPublisher(ProfileDetailsContract.UiEvent.FollowAction(state.profileId)) }, - onUnfollow = { eventPublisher(ProfileDetailsContract.UiEvent.UnfollowAction(state.profileId)) }, + onFollow = { + eventPublisher( + ProfileDetailsContract.UiEvent.FollowAction( + profileId = state.profileId, + forceUpdate = false, + ), + ) + }, + onUnfollow = { + eventPublisher( + ProfileDetailsContract.UiEvent.UnfollowAction( + profileId = state.profileId, + forceUpdate = false, + ), + ) + }, onDrawerQrCodeClick = onDrawerQrCodeClick, onFollowsClick = onFollowsClick, onProfileClick = onProfileClick, diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt index f6812e9a7..2fb317b05 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt @@ -296,6 +296,34 @@ fun ProfileDetailsScreen( } } } + if (state.shouldApproveFollow) { + ConfirmFollowUnfollowProfileAlertDialog( + onClose = { eventPublisher(ProfileDetailsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, + onActionConfirmed = { + eventPublisher( + ProfileDetailsContract.UiEvent.FollowAction( + profileId = state.profileId, + forceUpdate = true, + ), + ) + }, + profileAction = ProfileAction.Follow, + ) + } + if (state.shouldApproveUnfollow) { + ConfirmFollowUnfollowProfileAlertDialog( + onClose = { eventPublisher(ProfileDetailsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, + onActionConfirmed = { + eventPublisher( + ProfileDetailsContract.UiEvent.UnfollowAction( + profileId = state.profileId, + forceUpdate = true, + ), + ) + }, + profileAction = ProfileAction.Unfollow, + ) + } Scaffold( snackbarHost = { diff --git a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsContract.kt b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsContract.kt index 598de3b2f..8a7bc51bc 100644 --- a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsContract.kt +++ b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsContract.kt @@ -12,6 +12,8 @@ interface ProfileFollowsContract { val userFollowing: Set = emptySet(), val error: FollowsError? = null, val users: List = emptyList(), + val shouldApproveFollow: Boolean = false, + val shouldApproveUnfollow: Boolean = false, ) { sealed class FollowsError { data class FailedToFollowUser(val cause: Throwable) : FollowsError() @@ -21,8 +23,17 @@ interface ProfileFollowsContract { } sealed class UiEvent { - data class FollowProfile(val profileId: String) : UiEvent() - data class UnfollowProfile(val profileId: String) : UiEvent() + data class FollowProfile( + val profileId: String, + val forceUpdate: Boolean, + ) : UiEvent() + + data class UnfollowProfile( + val profileId: String, + val forceUpdate: Boolean, + ) : UiEvent() + + data object DismissConfirmFollowUnfollowAlertDialog : UiEvent() data object DismissError : UiEvent() data object ReloadData : UiEvent() } diff --git a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt index 12b73f040..3c322d1ad 100644 --- a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt @@ -12,7 +12,11 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -27,6 +31,8 @@ import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.ArrowBack import net.primal.android.explore.search.ui.FollowUnfollowVisibility import net.primal.android.explore.search.ui.UserProfileListItem +import net.primal.android.profile.details.ui.ConfirmFollowUnfollowProfileAlertDialog +import net.primal.android.profile.details.ui.ProfileAction import net.primal.android.profile.domain.ProfileFollowsType import net.primal.android.profile.follows.ProfileFollowsContract.UiState.FollowsError @@ -56,6 +62,7 @@ private fun ProfileFollowsScreen( ) { val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } + var lastFollowUnfollowProfileId by rememberSaveable { mutableStateOf(null) } SnackbarErrorHandler( error = state.error, snackbarHostState = snackbarHostState, @@ -69,6 +76,12 @@ private fun ProfileFollowsScreen( onErrorDismiss = { eventPublisher(ProfileFollowsContract.UiEvent.DismissError) }, ) + FollowApprovalAlertDialogs( + state = state, + eventPublisher = eventPublisher, + lastFollowUnfollowProfileId = lastFollowUnfollowProfileId, + ) + Scaffold( modifier = Modifier, topBar = { @@ -89,8 +102,24 @@ private fun ProfileFollowsScreen( paddingValues = paddingValues, state = state, onProfileClick = onProfileClick, - onFollowProfileClick = { eventPublisher(ProfileFollowsContract.UiEvent.FollowProfile(it)) }, - onUnfollowProfileClick = { eventPublisher(ProfileFollowsContract.UiEvent.UnfollowProfile(it)) }, + onFollowProfileClick = { + lastFollowUnfollowProfileId = it + eventPublisher( + ProfileFollowsContract.UiEvent.FollowProfile( + profileId = it, + forceUpdate = false, + ), + ) + }, + onUnfollowProfileClick = { + lastFollowUnfollowProfileId = it + eventPublisher( + ProfileFollowsContract.UiEvent.UnfollowProfile( + profileId = it, + forceUpdate = false, + ), + ) + }, onRefreshClick = { eventPublisher(ProfileFollowsContract.UiEvent.ReloadData) }, ) }, @@ -100,6 +129,46 @@ private fun ProfileFollowsScreen( ) } +@Composable +private fun FollowApprovalAlertDialogs( + state: ProfileFollowsContract.UiState, + eventPublisher: (ProfileFollowsContract.UiEvent) -> Unit, + lastFollowUnfollowProfileId: String?, +) { + if (state.shouldApproveFollow) { + ConfirmFollowUnfollowProfileAlertDialog( + onClose = { eventPublisher(ProfileFollowsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, + onActionConfirmed = { + lastFollowUnfollowProfileId?.let { + eventPublisher( + ProfileFollowsContract.UiEvent.FollowProfile( + profileId = it, + forceUpdate = true, + ), + ) + } + }, + profileAction = ProfileAction.Follow, + ) + } + if (state.shouldApproveUnfollow) { + ConfirmFollowUnfollowProfileAlertDialog( + onClose = { eventPublisher(ProfileFollowsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, + onActionConfirmed = { + lastFollowUnfollowProfileId?.let { + eventPublisher( + ProfileFollowsContract.UiEvent.UnfollowProfile( + profileId = it, + forceUpdate = true, + ), + ) + } + }, + profileAction = ProfileAction.Unfollow, + ) + } +} + @Composable private fun FollowsLazyColumn( paddingValues: PaddingValues, diff --git a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt index b437718b9..0b8359783 100644 --- a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt @@ -72,10 +72,12 @@ class ProfileFollowsViewModel @Inject constructor( viewModelScope.launch { events.collect { when (it) { - is UiEvent.FollowProfile -> follow(profileId = it.profileId) - is UiEvent.UnfollowProfile -> unfollow(profileId = it.profileId) + is UiEvent.FollowProfile -> follow(profileId = it.profileId, forceUpdate = it.forceUpdate) + is UiEvent.UnfollowProfile -> unfollow(profileId = it.profileId, forceUpdate = it.forceUpdate) UiEvent.DismissError -> setState { copy(error = null) } UiEvent.ReloadData -> fetchFollows() + UiEvent.DismissConfirmFollowUnfollowAlertDialog -> + setState { copy(shouldApproveFollow = false, shouldApproveUnfollow = false) } } } } @@ -124,13 +126,14 @@ class ProfileFollowsViewModel @Inject constructor( setState { copy(userFollowing = this.userFollowing.toMutableSet().apply { remove(profileId) }) } } - private fun follow(profileId: String) = + private fun follow(profileId: String, forceUpdate: Boolean) = viewModelScope.launch { updateStateProfileFollow(profileId) try { profileRepository.follow( userId = activeAccountStore.activeUserId(), followedUserId = profileId, + forceUpdate = forceUpdate, ) } catch (error: WssException) { Timber.w(error) @@ -148,16 +151,21 @@ class ProfileFollowsViewModel @Inject constructor( Timber.w(error) setErrorState(error = UiState.FollowsError.FailedToFollowUser(error)) updateStateProfileUnfollow(profileId) + } catch (error: ProfileRepository.PossibleFollowListCorruption) { + Timber.w(error) + updateStateProfileUnfollow(profileId) + setState { copy(shouldApproveFollow = true) } } } - private fun unfollow(profileId: String) = + private fun unfollow(profileId: String, forceUpdate: Boolean) = viewModelScope.launch { updateStateProfileUnfollow(profileId) try { profileRepository.unfollow( userId = activeAccountStore.activeUserId(), unfollowedUserId = profileId, + forceUpdate = forceUpdate, ) } catch (error: WssException) { Timber.w(error) @@ -175,6 +183,10 @@ class ProfileFollowsViewModel @Inject constructor( Timber.w(error) setErrorState(error = UiState.FollowsError.FailedToUnfollowUser(error)) updateStateProfileFollow(profileId) + } catch (error: ProfileRepository.PossibleFollowListCorruption) { + Timber.w(error) + updateStateProfileFollow(profileId) + setState { copy(shouldApproveUnfollow = true) } } } 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 fb812da8f..65a3b87d1 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 @@ -110,35 +110,50 @@ class ProfileRepository @Inject constructor( } } - @Throws(FollowListNotFound::class, NostrPublishException::class) - suspend fun follow(userId: String, followedUserId: String) { - updateFollowList(userId = userId) { + @Throws(FollowListNotFound::class, NostrPublishException::class, PossibleFollowListCorruption::class) + suspend fun follow( + userId: String, + followedUserId: String, + forceUpdate: Boolean, + ) { + updateFollowList(userId = userId, forceUpdate = forceUpdate) { toMutableSet().apply { add(followedUserId) } } } - @Throws(FollowListNotFound::class, NostrPublishException::class) - suspend fun unfollow(userId: String, unfollowedUserId: String) { - updateFollowList(userId = userId) { + @Throws(FollowListNotFound::class, NostrPublishException::class, PossibleFollowListCorruption::class) + suspend fun unfollow( + userId: String, + unfollowedUserId: String, + forceUpdate: Boolean, + ) { + updateFollowList(userId = userId, forceUpdate = forceUpdate) { toMutableSet().apply { remove(unfollowedUserId) } } } - @Throws(FollowListNotFound::class, NostrPublishException::class) - private suspend fun updateFollowList(userId: String, reducer: Set.() -> Set) = - withContext(dispatchers.io()) { - val userFollowList = userAccountFetcher.fetchUserFollowListOrNull(userId = userId) - ?: throw FollowListNotFound() - - userRepository.updateFollowList(userId, userFollowList) + @Throws(FollowListNotFound::class, NostrPublishException::class, PossibleFollowListCorruption::class) + private suspend fun updateFollowList( + userId: String, + forceUpdate: Boolean, + reducer: Set.() -> Set, + ) = withContext(dispatchers.io()) { + val userFollowList = userAccountFetcher.fetchUserFollowListOrNull(userId = userId) + ?: throw FollowListNotFound() - setFollowList( - userId = userId, - contacts = userFollowList.following.reducer(), - content = userFollowList.followListEventContent ?: "", - ) + if (userFollowList.following.isEmpty() && !forceUpdate) { + throw PossibleFollowListCorruption() } + userRepository.updateFollowList(userId, userFollowList) + + setFollowList( + userId = userId, + contacts = userFollowList.following.reducer(), + content = userFollowList.followListEventContent ?: "", + ) + } + @Throws(NostrPublishException::class) suspend fun setFollowList( userId: String, @@ -251,4 +266,5 @@ class ProfileRepository @Inject constructor( } class FollowListNotFound : Exception() + class PossibleFollowListCorruption : Exception() } diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsViewModel.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsViewModel.kt index c221197db..f928b8325 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsViewModel.kt @@ -275,11 +275,13 @@ class ArticleDetailsViewModel @Inject constructor( profileRepository.unfollow( userId = activeAccountStore.activeUserId(), unfollowedUserId = article.authorId, + forceUpdate = false, ) } else { profileRepository.follow( userId = activeAccountStore.activeUserId(), followedUserId = article.authorId, + forceUpdate = false, ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 750cb1e0c..18f53d997 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -825,6 +825,16 @@ Save Bookmark No + Saving first follow + This will result in following a total of one account. Do you wish to continue? + Save follow + No + + Saving unfollow + This will result in following a total of zero accounts. Do you wish to continue? + Save unfollow + No + Report abuse All reports posted will be publicly visible. Dismiss From 8ac8c57bfc8fbbe454430de823b4049eb2a1a89a Mon Sep 17 00:00:00 2001 From: Aleksandar Ilic Date: Fri, 29 Nov 2024 17:05:12 +0100 Subject: [PATCH 2/7] Move bookmark approval dialog to core.compose.profile.approvals --- .../net/primal/android/articles/feed/ArticleFeedList.kt | 4 ++-- .../compose/profile/approvals/ApproveBookmarkAlertDialog.kt} | 4 ++-- .../kotlin/net/primal/android/notes/feed/note/FeedNoteCard.kt | 4 ++-- .../android/thread/articles/details/ArticleDetailsScreen.kt | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename app/src/main/kotlin/net/primal/android/{notes/feed/note/ui/ConfirmFirstBookmarkAlertDialog.kt => core/compose/profile/approvals/ApproveBookmarkAlertDialog.kt} (89%) diff --git a/app/src/main/kotlin/net/primal/android/articles/feed/ArticleFeedList.kt b/app/src/main/kotlin/net/primal/android/articles/feed/ArticleFeedList.kt index a846c02e7..5d212ee98 100644 --- a/app/src/main/kotlin/net/primal/android/articles/feed/ArticleFeedList.kt +++ b/app/src/main/kotlin/net/primal/android/articles/feed/ArticleFeedList.kt @@ -52,9 +52,9 @@ import net.primal.android.core.compose.foundation.rememberLazyListStatePagingWor import net.primal.android.core.compose.heightAdjustableLoadingLazyListPlaceholder import net.primal.android.core.compose.isEmpty import net.primal.android.core.compose.isNotEmpty +import net.primal.android.core.compose.profile.approvals.ApproveBookmarkAlertDialog import net.primal.android.core.compose.pulltorefresh.PrimalPullToRefreshBox import net.primal.android.core.errors.UiError -import net.primal.android.notes.feed.note.ui.ConfirmFirstBookmarkAlertDialog import net.primal.android.theme.AppTheme import net.primal.android.thread.articles.ArticleContract import net.primal.android.thread.articles.ArticleViewModel @@ -239,7 +239,7 @@ private fun ArticleFeedLazyColumn( when { item != null -> Column { if (articleState.shouldApproveBookmark) { - ConfirmFirstBookmarkAlertDialog( + ApproveBookmarkAlertDialog( onBookmarkConfirmed = { articleEventPublisher( ArticleContract.UiEvent.BookmarkAction( diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/ConfirmFirstBookmarkAlertDialog.kt b/app/src/main/kotlin/net/primal/android/core/compose/profile/approvals/ApproveBookmarkAlertDialog.kt similarity index 89% rename from app/src/main/kotlin/net/primal/android/notes/feed/note/ui/ConfirmFirstBookmarkAlertDialog.kt rename to app/src/main/kotlin/net/primal/android/core/compose/profile/approvals/ApproveBookmarkAlertDialog.kt index f655f2d16..375367bfa 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/ConfirmFirstBookmarkAlertDialog.kt +++ b/app/src/main/kotlin/net/primal/android/core/compose/profile/approvals/ApproveBookmarkAlertDialog.kt @@ -1,4 +1,4 @@ -package net.primal.android.notes.feed.note.ui +package net.primal.android.core.compose.profile.approvals import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text @@ -9,7 +9,7 @@ import net.primal.android.R import net.primal.android.theme.AppTheme @Composable -fun ConfirmFirstBookmarkAlertDialog(onClose: () -> Unit, onBookmarkConfirmed: () -> Unit) { +fun ApproveBookmarkAlertDialog(onClose: () -> Unit, onBookmarkConfirmed: () -> Unit) { AlertDialog( containerColor = AppTheme.colorScheme.surfaceVariant, onDismissRequest = onClose, diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/note/FeedNoteCard.kt b/app/src/main/kotlin/net/primal/android/notes/feed/note/FeedNoteCard.kt index 194c3a601..c78390596 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/note/FeedNoteCard.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/note/FeedNoteCard.kt @@ -47,6 +47,7 @@ import net.primal.android.attachments.domain.CdnImage import net.primal.android.core.compose.AvatarThumbnailCustomBorder import net.primal.android.core.compose.PrimalDivider import net.primal.android.core.compose.preview.PrimalPreview +import net.primal.android.core.compose.profile.approvals.ApproveBookmarkAlertDialog import net.primal.android.core.errors.UiError import net.primal.android.core.ext.openUriSafely import net.primal.android.notes.feed.NoteRepostOrQuoteBottomSheet @@ -55,7 +56,6 @@ import net.primal.android.notes.feed.model.FeedPostAction import net.primal.android.notes.feed.model.FeedPostUi import net.primal.android.notes.feed.model.toNoteContentUi import net.primal.android.notes.feed.note.NoteContract.UiEvent -import net.primal.android.notes.feed.note.ui.ConfirmFirstBookmarkAlertDialog import net.primal.android.notes.feed.note.ui.FeedNoteActionsRow import net.primal.android.notes.feed.note.ui.FeedNoteHeader import net.primal.android.notes.feed.note.ui.NoteContent @@ -221,7 +221,7 @@ private fun FeedNoteCard( } if (state.shouldApproveBookmark) { - ConfirmFirstBookmarkAlertDialog( + ApproveBookmarkAlertDialog( onBookmarkConfirmed = { eventPublisher( UiEvent.BookmarkAction( diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsScreen.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsScreen.kt index 0a1cb1153..60983f246 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsScreen.kt @@ -67,6 +67,7 @@ import net.primal.android.core.compose.icons.primaliconpack.Bookmarks import net.primal.android.core.compose.icons.primaliconpack.BookmarksFilled import net.primal.android.core.compose.icons.primaliconpack.FeedNewZapFilled import net.primal.android.core.compose.icons.primaliconpack.More +import net.primal.android.core.compose.profile.approvals.ApproveBookmarkAlertDialog import net.primal.android.core.compose.runtime.DisposableLifecycleObserverEffect import net.primal.android.core.compose.zaps.ArticleTopZapsSection import net.primal.android.core.errors.UiError @@ -83,7 +84,6 @@ import net.primal.android.notes.feed.model.EventStatsUi import net.primal.android.notes.feed.model.FeedPostAction import net.primal.android.notes.feed.model.FeedPostUi import net.primal.android.notes.feed.note.FeedNoteCard -import net.primal.android.notes.feed.note.ui.ConfirmFirstBookmarkAlertDialog import net.primal.android.notes.feed.note.ui.FeedNoteActionsRow import net.primal.android.notes.feed.note.ui.ReferencedNoteCard import net.primal.android.notes.feed.note.ui.ThreadNoteStatsRow @@ -217,7 +217,7 @@ private fun ArticleDetailsScreen( } if (articleState.shouldApproveBookmark && detailsState.article != null) { - ConfirmFirstBookmarkAlertDialog( + ApproveBookmarkAlertDialog( onBookmarkConfirmed = { articleEventPublisher( ArticleContract.UiEvent.BookmarkAction( From 1e049b5fecb9dd0230eb76a652a4e778c5f1691f Mon Sep 17 00:00:00 2001 From: Aleksandar Ilic Date: Fri, 29 Nov 2024 17:10:26 +0100 Subject: [PATCH 3/7] Move follow approval dialog to core.compose.profile.approvals --- .../approvals/ApproveFollowUnfollowProfileAlertDialog.kt} | 5 +++-- .../primal/android/explore/home/people/ExplorePeople.kt | 8 ++++---- .../android/profile/details/ui/ProfileDetailsScreen.kt | 6 ++++-- .../android/profile/follows/ProfileFollowsScreen.kt | 8 ++++---- 4 files changed, 15 insertions(+), 12 deletions(-) rename app/src/main/kotlin/net/primal/android/{profile/details/ui/ConfirmFollowUnfollowProfileAlertDialog.kt => core/compose/profile/approvals/ApproveFollowUnfollowProfileAlertDialog.kt} (95%) diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ui/ConfirmFollowUnfollowProfileAlertDialog.kt b/app/src/main/kotlin/net/primal/android/core/compose/profile/approvals/ApproveFollowUnfollowProfileAlertDialog.kt similarity index 95% rename from app/src/main/kotlin/net/primal/android/profile/details/ui/ConfirmFollowUnfollowProfileAlertDialog.kt rename to app/src/main/kotlin/net/primal/android/core/compose/profile/approvals/ApproveFollowUnfollowProfileAlertDialog.kt index 4481b3881..b2b24edec 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ui/ConfirmFollowUnfollowProfileAlertDialog.kt +++ b/app/src/main/kotlin/net/primal/android/core/compose/profile/approvals/ApproveFollowUnfollowProfileAlertDialog.kt @@ -1,4 +1,4 @@ -package net.primal.android.profile.details.ui +package net.primal.android.core.compose.profile.approvals import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text @@ -9,7 +9,7 @@ import net.primal.android.R import net.primal.android.theme.AppTheme @Composable -fun ConfirmFollowUnfollowProfileAlertDialog( +fun ApproveFollowUnfollowProfileAlertDialog( onClose: () -> Unit, onActionConfirmed: () -> Unit, profileAction: ProfileAction, @@ -23,6 +23,7 @@ fun ConfirmFollowUnfollowProfileAlertDialog( negative = stringResource(id = R.string.context_confirm_follow_negative), ) } + ProfileAction.Unfollow -> { ApprovalMessages( title = stringResource(id = R.string.context_confirm_unfollow_title), diff --git a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt index 886ef9ec3..b4fc45fca 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt @@ -45,12 +45,12 @@ import net.primal.android.core.compose.ListNoContent import net.primal.android.core.compose.NostrUserText import net.primal.android.core.compose.button.FollowUnfollowButton import net.primal.android.core.compose.preview.PrimalPreview +import net.primal.android.core.compose.profile.approvals.ApproveFollowUnfollowProfileAlertDialog +import net.primal.android.core.compose.profile.approvals.ProfileAction import net.primal.android.core.compose.profile.model.ProfileDetailsUi import net.primal.android.core.errors.UiError import net.primal.android.core.utils.shortened import net.primal.android.explore.api.model.ExplorePeopleData -import net.primal.android.profile.details.ui.ConfirmFollowUnfollowProfileAlertDialog -import net.primal.android.profile.details.ui.ProfileAction import net.primal.android.theme.AppTheme import net.primal.android.theme.domain.PrimalTheme @@ -158,7 +158,7 @@ private fun ApprovalAlertDialogs( lastFollowUnfollowProfileId: String?, ) { if (state.shouldApproveFollow) { - ConfirmFollowUnfollowProfileAlertDialog( + ApproveFollowUnfollowProfileAlertDialog( onClose = { eventPublisher(ExplorePeopleContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, onActionConfirmed = { lastFollowUnfollowProfileId?.let { @@ -174,7 +174,7 @@ private fun ApprovalAlertDialogs( ) } if (state.shouldApproveUnfollow) { - ConfirmFollowUnfollowProfileAlertDialog( + ApproveFollowUnfollowProfileAlertDialog( onClose = { eventPublisher(ExplorePeopleContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, onActionConfirmed = { lastFollowUnfollowProfileId?.let { diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt index 2fb317b05..361d59da2 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt @@ -63,6 +63,8 @@ import net.primal.android.R import net.primal.android.articles.feed.ArticleFeedList import net.primal.android.core.compose.SnackbarErrorHandler import net.primal.android.core.compose.preview.PrimalPreview +import net.primal.android.core.compose.profile.approvals.ApproveFollowUnfollowProfileAlertDialog +import net.primal.android.core.compose.profile.approvals.ProfileAction import net.primal.android.core.compose.profile.model.ProfileDetailsUi import net.primal.android.core.compose.pulltorefresh.PrimalPullToRefreshBox import net.primal.android.core.compose.runtime.DisposableLifecycleObserverEffect @@ -297,7 +299,7 @@ fun ProfileDetailsScreen( } } if (state.shouldApproveFollow) { - ConfirmFollowUnfollowProfileAlertDialog( + ApproveFollowUnfollowProfileAlertDialog( onClose = { eventPublisher(ProfileDetailsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, onActionConfirmed = { eventPublisher( @@ -311,7 +313,7 @@ fun ProfileDetailsScreen( ) } if (state.shouldApproveUnfollow) { - ConfirmFollowUnfollowProfileAlertDialog( + ApproveFollowUnfollowProfileAlertDialog( onClose = { eventPublisher(ProfileDetailsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, onActionConfirmed = { eventPublisher( diff --git a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt index 3c322d1ad..85c5bb492 100644 --- a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt @@ -29,10 +29,10 @@ import net.primal.android.core.compose.SnackbarErrorHandler import net.primal.android.core.compose.heightAdjustableLoadingLazyListPlaceholder import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.ArrowBack +import net.primal.android.core.compose.profile.approvals.ApproveFollowUnfollowProfileAlertDialog +import net.primal.android.core.compose.profile.approvals.ProfileAction import net.primal.android.explore.search.ui.FollowUnfollowVisibility import net.primal.android.explore.search.ui.UserProfileListItem -import net.primal.android.profile.details.ui.ConfirmFollowUnfollowProfileAlertDialog -import net.primal.android.profile.details.ui.ProfileAction import net.primal.android.profile.domain.ProfileFollowsType import net.primal.android.profile.follows.ProfileFollowsContract.UiState.FollowsError @@ -136,7 +136,7 @@ private fun FollowApprovalAlertDialogs( lastFollowUnfollowProfileId: String?, ) { if (state.shouldApproveFollow) { - ConfirmFollowUnfollowProfileAlertDialog( + ApproveFollowUnfollowProfileAlertDialog( onClose = { eventPublisher(ProfileFollowsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, onActionConfirmed = { lastFollowUnfollowProfileId?.let { @@ -152,7 +152,7 @@ private fun FollowApprovalAlertDialogs( ) } if (state.shouldApproveUnfollow) { - ConfirmFollowUnfollowProfileAlertDialog( + ApproveFollowUnfollowProfileAlertDialog( onClose = { eventPublisher(ProfileFollowsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, onActionConfirmed = { lastFollowUnfollowProfileId?.let { From 8c4c02e49c97be7c6bd26d911af62b951279bfab Mon Sep 17 00:00:00 2001 From: Aleksandar Ilic Date: Fri, 29 Nov 2024 22:16:16 +0100 Subject: [PATCH 4/7] Implement new business logic for follow/unfollow approvals --- ...ApproveFollowUnfollowProfileAlertDialog.kt | 16 ++-- .../explore/home/people/ExplorePeople.kt | 75 ++++++------------- .../home/people/ExplorePeopleContract.kt | 15 +--- .../home/people/ExplorePeopleViewModel.kt | 41 +++++----- .../profile/details/ProfileDetailsContract.kt | 15 +--- .../details/ProfileDetailsViewModel.kt | 19 ++--- .../details/ui/ProfileDetailsScreen.kt | 39 +++++----- .../profile/follows/ProfileFollowsContract.kt | 16 +--- .../profile/follows/ProfileFollowsScreen.kt | 73 ++++++------------ .../follows/ProfileFollowsViewModel.kt | 15 +--- .../profile/repository/ProfileRepository.kt | 23 +++--- 11 files changed, 122 insertions(+), 225 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/core/compose/profile/approvals/ApproveFollowUnfollowProfileAlertDialog.kt b/app/src/main/kotlin/net/primal/android/core/compose/profile/approvals/ApproveFollowUnfollowProfileAlertDialog.kt index b2b24edec..52dc1fd45 100644 --- a/app/src/main/kotlin/net/primal/android/core/compose/profile/approvals/ApproveFollowUnfollowProfileAlertDialog.kt +++ b/app/src/main/kotlin/net/primal/android/core/compose/profile/approvals/ApproveFollowUnfollowProfileAlertDialog.kt @@ -10,12 +10,12 @@ import net.primal.android.theme.AppTheme @Composable fun ApproveFollowUnfollowProfileAlertDialog( - onClose: () -> Unit, - onActionConfirmed: () -> Unit, profileAction: ProfileAction, + onActionApproved: () -> Unit, + onClose: () -> Unit, ) { val messages = when (profileAction) { - ProfileAction.Follow -> { + is ProfileAction.Follow -> { ApprovalMessages( title = stringResource(id = R.string.context_confirm_follow_title), text = stringResource(id = R.string.context_confirm_follow_text), @@ -24,7 +24,7 @@ fun ApproveFollowUnfollowProfileAlertDialog( ) } - ProfileAction.Unfollow -> { + is ProfileAction.Unfollow -> { ApprovalMessages( title = stringResource(id = R.string.context_confirm_unfollow_title), text = stringResource(id = R.string.context_confirm_unfollow_text), @@ -54,7 +54,7 @@ fun ApproveFollowUnfollowProfileAlertDialog( } }, confirmButton = { - TextButton(onClick = onActionConfirmed) { + TextButton(onClick = onActionApproved) { Text( text = messages.positive, ) @@ -70,7 +70,7 @@ private data class ApprovalMessages( val negative: String, ) -enum class ProfileAction { - Follow, - Unfollow, +sealed class ProfileAction(open val profileId: String) { + data class Follow(override val profileId: String) : ProfileAction(profileId) + data class Unfollow(override val profileId: String) : ProfileAction(profileId) } diff --git a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt index b4fc45fca..eb4266c6a 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt @@ -21,9 +21,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -86,13 +83,26 @@ fun ExplorePeople( eventPublisher: (ExplorePeopleContract.UiEvent) -> Unit, onProfileClick: (String) -> Unit, ) { - var lastFollowUnfollowProfileId by rememberSaveable { mutableStateOf(null) } + if (state.shouldApproveProfileAction != null) { + ApproveFollowUnfollowProfileAlertDialog( + profileAction = state.shouldApproveProfileAction, + onActionApproved = { + val event = when (state.shouldApproveProfileAction) { + is ProfileAction.Follow -> ExplorePeopleContract.UiEvent.FollowUser( + userId = state.shouldApproveProfileAction.profileId, + forceUpdate = true, + ) - ApprovalAlertDialogs( - state = state, - eventPublisher = eventPublisher, - lastFollowUnfollowProfileId = lastFollowUnfollowProfileId, - ) + is ProfileAction.Unfollow -> ExplorePeopleContract.UiEvent.UnfollowUser( + userId = state.shouldApproveProfileAction.profileId, + forceUpdate = true, + ) + } + eventPublisher(event) + }, + onClose = { eventPublisher(ExplorePeopleContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, + ) + } if (state.loading && state.people.isEmpty()) { HeightAdjustableLoadingLazyListPlaceholder( @@ -129,13 +139,14 @@ fun ExplorePeople( isFollowed = state.userFollowing.contains(item.profile.pubkey), onItemClick = { onProfileClick(item.profile.pubkey) }, onFollowClick = { - lastFollowUnfollowProfileId = item.profile.pubkey eventPublisher( - ExplorePeopleContract.UiEvent.FollowUser(userId = item.profile.pubkey, forceUpdate = false), + ExplorePeopleContract.UiEvent.FollowUser( + userId = item.profile.pubkey, + forceUpdate = false, + ), ) }, onUnfollowClick = { - lastFollowUnfollowProfileId = item.profile.pubkey eventPublisher( ExplorePeopleContract.UiEvent.UnfollowUser( userId = item.profile.pubkey, @@ -151,46 +162,6 @@ fun ExplorePeople( } } -@Composable -private fun ApprovalAlertDialogs( - state: ExplorePeopleContract.UiState, - eventPublisher: (ExplorePeopleContract.UiEvent) -> Unit, - lastFollowUnfollowProfileId: String?, -) { - if (state.shouldApproveFollow) { - ApproveFollowUnfollowProfileAlertDialog( - onClose = { eventPublisher(ExplorePeopleContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, - onActionConfirmed = { - lastFollowUnfollowProfileId?.let { - eventPublisher( - ExplorePeopleContract.UiEvent.FollowUser( - userId = it, - forceUpdate = true, - ), - ) - } - }, - profileAction = ProfileAction.Follow, - ) - } - if (state.shouldApproveUnfollow) { - ApproveFollowUnfollowProfileAlertDialog( - onClose = { eventPublisher(ExplorePeopleContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, - onActionConfirmed = { - lastFollowUnfollowProfileId?.let { - eventPublisher( - ExplorePeopleContract.UiEvent.UnfollowUser( - userId = it, - forceUpdate = true, - ), - ) - } - }, - profileAction = ProfileAction.Unfollow, - ) - } -} - @Composable private fun ExplorePersonListItem( modifier: Modifier = Modifier, diff --git a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleContract.kt b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleContract.kt index eb9608ffc..04990e994 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleContract.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleContract.kt @@ -1,5 +1,6 @@ package net.primal.android.explore.home.people +import net.primal.android.core.compose.profile.approvals.ProfileAction import net.primal.android.core.errors.UiError import net.primal.android.explore.api.model.ExplorePeopleData @@ -10,20 +11,12 @@ interface ExplorePeopleContract { val people: List = emptyList(), val userFollowing: Set = emptySet(), val error: UiError? = null, - val shouldApproveFollow: Boolean = false, - val shouldApproveUnfollow: Boolean = false, + val shouldApproveProfileAction: ProfileAction? = null, ) sealed class UiEvent { - data class FollowUser( - val userId: String, - val forceUpdate: Boolean, - ) : UiEvent() - data class UnfollowUser( - val userId: String, - val forceUpdate: Boolean, - ) : UiEvent() - + data class FollowUser(val userId: String, val forceUpdate: Boolean) : UiEvent() + data class UnfollowUser(val userId: String, val forceUpdate: Boolean) : UiEvent() data object DismissConfirmFollowUnfollowAlertDialog : UiEvent() data object RefreshPeople : UiEvent() data object DismissError : UiEvent() diff --git a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt index bf138f40e..93f55dfb1 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.launch +import net.primal.android.core.compose.profile.approvals.ProfileAction import net.primal.android.core.errors.UiError import net.primal.android.explore.home.people.ExplorePeopleContract.UiEvent import net.primal.android.explore.home.people.ExplorePeopleContract.UiState @@ -81,7 +82,7 @@ class ExplorePeopleViewModel @Inject constructor( UiEvent.RefreshPeople -> fetchExplorePeople() UiEvent.DismissError -> setState { copy(error = null) } UiEvent.DismissConfirmFollowUnfollowAlertDialog -> - setState { copy(shouldApproveFollow = false, shouldApproveUnfollow = false) } + setState { copy(shouldApproveProfileAction = null) } } } } @@ -102,20 +103,14 @@ class ExplorePeopleViewModel @Inject constructor( followResult.exceptionOrNull()?.let { error -> Timber.w(error) when (error) { - is WssException, is NostrPublishException, is ProfileRepository.FollowListNotFound -> + is WssException, is NostrPublishException -> setState { copy(error = UiError.FailedToFollowUser(error)) } - is ProfileRepository.PossibleFollowListCorruption -> setState { - copy( - shouldApproveFollow = true, - ) - } + is ProfileRepository.FollowListNotFound -> + setState { copy(shouldApproveProfileAction = ProfileAction.Follow(profileId = profileId)) } - is MissingRelaysException -> setState { - copy( - error = UiError.MissingRelaysConfiguration(error), - ) - } + is MissingRelaysException -> + setState { copy(error = UiError.MissingRelaysConfiguration(error)) } else -> setState { copy(error = UiError.GenericError()) } } @@ -140,20 +135,18 @@ class ExplorePeopleViewModel @Inject constructor( unfollowResult.exceptionOrNull()?.let { error -> Timber.w(error) when (error) { - is WssException, is NostrPublishException, is ProfileRepository.FollowListNotFound -> + is WssException, is NostrPublishException -> setState { copy(error = UiError.FailedToUnfollowUser(error)) } - is ProfileRepository.PossibleFollowListCorruption -> setState { - copy( - shouldApproveUnfollow = true, - ) - } - - is MissingRelaysException -> setState { - copy( - error = UiError.MissingRelaysConfiguration(error), - ) - } + is ProfileRepository.FollowListNotFound -> + setState { + copy( + shouldApproveProfileAction = ProfileAction.Unfollow(profileId = profileId), + ) + } + + is MissingRelaysException -> + setState { copy(error = UiError.MissingRelaysConfiguration(error)) } else -> setState { copy(error = UiError.GenericError()) } } diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsContract.kt b/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsContract.kt index 6dca7d7bc..8c998bf37 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsContract.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsContract.kt @@ -1,5 +1,6 @@ package net.primal.android.profile.details +import net.primal.android.core.compose.profile.approvals.ProfileAction import net.primal.android.core.compose.profile.model.ProfileDetailsUi import net.primal.android.core.compose.profile.model.ProfileStatsUi import net.primal.android.core.errors.UiError @@ -26,8 +27,7 @@ interface ProfileDetailsContract { ProfileFeedSpec.AuthoredMedia, ), val error: ProfileError? = null, - val shouldApproveFollow: Boolean = false, - val shouldApproveUnfollow: Boolean = false, + val shouldApproveProfileAction: ProfileAction? = null, val zapError: UiError? = null, val zappingState: ZappingState = ZappingState(), ) { @@ -50,14 +50,6 @@ interface ProfileDetailsContract { } sealed class UiEvent { - data class FollowAction( - val profileId: String, - val forceUpdate: Boolean, - ) : UiEvent() - data class UnfollowAction( - val profileId: String, - val forceUpdate: Boolean, - ) : UiEvent() data class AddProfileFeedAction( val profileId: String, val feedTitle: String, @@ -69,7 +61,8 @@ interface ProfileDetailsContract { val zapDescription: String? = null, val zapAmount: ULong? = null, ) : UiEvent() - + data class FollowAction(val profileId: String, val forceUpdate: Boolean) : UiEvent() + data class UnfollowAction(val profileId: String, val forceUpdate: Boolean) : UiEvent() data class RemoveProfileFeedAction(val profileId: String) : UiEvent() data class MuteAction(val profileId: String) : UiEvent() data class UnmuteAction(val profileId: String) : UiEvent() diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt b/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt index 41cdf020c..f6ed80cd2 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import net.primal.android.core.compose.profile.approvals.ProfileAction import net.primal.android.core.compose.profile.model.asProfileDetailsUi import net.primal.android.core.compose.profile.model.asProfileStatsUi import net.primal.android.core.coroutines.CoroutineDispatcherProvider @@ -121,7 +122,7 @@ class ProfileDetailsViewModel @Inject constructor( UiEvent.DismissZapError -> setState { copy(zapError = null) } UiEvent.DismissConfirmFollowUnfollowAlertDialog -> - setState { copy(shouldApproveFollow = false, shouldApproveUnfollow = false) } + setState { copy(shouldApproveProfileAction = null) } } } } @@ -330,11 +331,7 @@ class ProfileDetailsViewModel @Inject constructor( } catch (error: ProfileRepository.FollowListNotFound) { Timber.w(error) updateStateProfileAsUnfollowed() - setErrorState(error = ProfileError.FailedToFollowProfile(error)) - } catch (error: ProfileRepository.PossibleFollowListCorruption) { - Timber.w(error) - updateStateProfileAsUnfollowed() - setState { copy(shouldApproveFollow = true) } + setState { copy(shouldApproveProfileAction = ProfileAction.Follow(profileId = followAction.profileId)) } } } @@ -362,11 +359,11 @@ class ProfileDetailsViewModel @Inject constructor( } catch (error: ProfileRepository.FollowListNotFound) { Timber.w(error) updateStateProfileAsFollowed() - setErrorState(error = ProfileError.FailedToUnfollowProfile(error)) - } catch (error: ProfileRepository.PossibleFollowListCorruption) { - Timber.w(error) - updateStateProfileAsFollowed() - setState { copy(shouldApproveUnfollow = true) } + setState { + copy( + shouldApproveProfileAction = ProfileAction.Unfollow(profileId = unfollowAction.profileId), + ) + } } } diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt index 361d59da2..c0d5346bf 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt @@ -298,32 +298,27 @@ fun ProfileDetailsScreen( } } } - if (state.shouldApproveFollow) { + + if (state.shouldApproveProfileAction != null) { ApproveFollowUnfollowProfileAlertDialog( - onClose = { eventPublisher(ProfileDetailsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, - onActionConfirmed = { - eventPublisher( - ProfileDetailsContract.UiEvent.FollowAction( - profileId = state.profileId, + profileAction = state.shouldApproveProfileAction, + onActionApproved = { + val event = when (state.shouldApproveProfileAction) { + is ProfileAction.Follow -> ProfileDetailsContract.UiEvent.FollowAction( + profileId = state.shouldApproveProfileAction.profileId, forceUpdate = true, - ), - ) - }, - profileAction = ProfileAction.Follow, - ) - } - if (state.shouldApproveUnfollow) { - ApproveFollowUnfollowProfileAlertDialog( - onClose = { eventPublisher(ProfileDetailsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, - onActionConfirmed = { - eventPublisher( - ProfileDetailsContract.UiEvent.UnfollowAction( - profileId = state.profileId, + ) + + is ProfileAction.Unfollow -> ProfileDetailsContract.UiEvent.UnfollowAction( + profileId = state.shouldApproveProfileAction.profileId, forceUpdate = true, - ), - ) + ) + } + eventPublisher(event) + }, + onClose = { + eventPublisher(ProfileDetailsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, - profileAction = ProfileAction.Unfollow, ) } diff --git a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsContract.kt b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsContract.kt index 8a7bc51bc..ae2214062 100644 --- a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsContract.kt +++ b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsContract.kt @@ -1,5 +1,6 @@ package net.primal.android.profile.follows +import net.primal.android.core.compose.profile.approvals.ProfileAction import net.primal.android.core.compose.profile.model.UserProfileItemUi import net.primal.android.profile.domain.ProfileFollowsType @@ -12,8 +13,7 @@ interface ProfileFollowsContract { val userFollowing: Set = emptySet(), val error: FollowsError? = null, val users: List = emptyList(), - val shouldApproveFollow: Boolean = false, - val shouldApproveUnfollow: Boolean = false, + val shouldApproveProfileAction: ProfileAction? = null, ) { sealed class FollowsError { data class FailedToFollowUser(val cause: Throwable) : FollowsError() @@ -23,16 +23,8 @@ interface ProfileFollowsContract { } sealed class UiEvent { - data class FollowProfile( - val profileId: String, - val forceUpdate: Boolean, - ) : UiEvent() - - data class UnfollowProfile( - val profileId: String, - val forceUpdate: Boolean, - ) : UiEvent() - + data class FollowProfile(val profileId: String, val forceUpdate: Boolean) : UiEvent() + data class UnfollowProfile(val profileId: String, val forceUpdate: Boolean) : UiEvent() data object DismissConfirmFollowUnfollowAlertDialog : UiEvent() data object DismissError : UiEvent() data object ReloadData : UiEvent() diff --git a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt index 85c5bb492..eb820ad09 100644 --- a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt @@ -12,11 +12,7 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -62,7 +58,7 @@ private fun ProfileFollowsScreen( ) { val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } - var lastFollowUnfollowProfileId by rememberSaveable { mutableStateOf(null) } + SnackbarErrorHandler( error = state.error, snackbarHostState = snackbarHostState, @@ -76,11 +72,26 @@ private fun ProfileFollowsScreen( onErrorDismiss = { eventPublisher(ProfileFollowsContract.UiEvent.DismissError) }, ) - FollowApprovalAlertDialogs( - state = state, - eventPublisher = eventPublisher, - lastFollowUnfollowProfileId = lastFollowUnfollowProfileId, - ) + if (state.shouldApproveProfileAction != null) { + ApproveFollowUnfollowProfileAlertDialog( + profileAction = state.shouldApproveProfileAction, + onActionApproved = { + val event = when (state.shouldApproveProfileAction) { + is ProfileAction.Follow -> ProfileFollowsContract.UiEvent.FollowProfile( + profileId = state.shouldApproveProfileAction.profileId, + forceUpdate = true, + ) + + is ProfileAction.Unfollow -> ProfileFollowsContract.UiEvent.UnfollowProfile( + profileId = state.shouldApproveProfileAction.profileId, + forceUpdate = true, + ) + } + eventPublisher(event) + }, + onClose = { eventPublisher(ProfileFollowsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, + ) + } Scaffold( modifier = Modifier, @@ -103,7 +114,6 @@ private fun ProfileFollowsScreen( state = state, onProfileClick = onProfileClick, onFollowProfileClick = { - lastFollowUnfollowProfileId = it eventPublisher( ProfileFollowsContract.UiEvent.FollowProfile( profileId = it, @@ -112,7 +122,6 @@ private fun ProfileFollowsScreen( ) }, onUnfollowProfileClick = { - lastFollowUnfollowProfileId = it eventPublisher( ProfileFollowsContract.UiEvent.UnfollowProfile( profileId = it, @@ -129,46 +138,6 @@ private fun ProfileFollowsScreen( ) } -@Composable -private fun FollowApprovalAlertDialogs( - state: ProfileFollowsContract.UiState, - eventPublisher: (ProfileFollowsContract.UiEvent) -> Unit, - lastFollowUnfollowProfileId: String?, -) { - if (state.shouldApproveFollow) { - ApproveFollowUnfollowProfileAlertDialog( - onClose = { eventPublisher(ProfileFollowsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, - onActionConfirmed = { - lastFollowUnfollowProfileId?.let { - eventPublisher( - ProfileFollowsContract.UiEvent.FollowProfile( - profileId = it, - forceUpdate = true, - ), - ) - } - }, - profileAction = ProfileAction.Follow, - ) - } - if (state.shouldApproveUnfollow) { - ApproveFollowUnfollowProfileAlertDialog( - onClose = { eventPublisher(ProfileFollowsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, - onActionConfirmed = { - lastFollowUnfollowProfileId?.let { - eventPublisher( - ProfileFollowsContract.UiEvent.UnfollowProfile( - profileId = it, - forceUpdate = true, - ), - ) - } - }, - profileAction = ProfileAction.Unfollow, - ) - } -} - @Composable private fun FollowsLazyColumn( paddingValues: PaddingValues, diff --git a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt index 0b8359783..ed64aad3e 100644 --- a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import net.primal.android.core.compose.profile.approvals.ProfileAction import net.primal.android.core.compose.profile.model.mapAsUserProfileUi import net.primal.android.core.coroutines.CoroutineDispatcherProvider import net.primal.android.core.utils.usernameUiFriendly @@ -77,7 +78,7 @@ class ProfileFollowsViewModel @Inject constructor( UiEvent.DismissError -> setState { copy(error = null) } UiEvent.ReloadData -> fetchFollows() UiEvent.DismissConfirmFollowUnfollowAlertDialog -> - setState { copy(shouldApproveFollow = false, shouldApproveUnfollow = false) } + setState { copy(shouldApproveProfileAction = null) } } } } @@ -148,13 +149,9 @@ class ProfileFollowsViewModel @Inject constructor( setErrorState(error = UiState.FollowsError.MissingRelaysConfiguration(error)) updateStateProfileUnfollow(profileId) } catch (error: ProfileRepository.FollowListNotFound) { - Timber.w(error) - setErrorState(error = UiState.FollowsError.FailedToFollowUser(error)) - updateStateProfileUnfollow(profileId) - } catch (error: ProfileRepository.PossibleFollowListCorruption) { Timber.w(error) updateStateProfileUnfollow(profileId) - setState { copy(shouldApproveFollow = true) } + setState { copy(shouldApproveProfileAction = ProfileAction.Follow(profileId = profileId)) } } } @@ -180,13 +177,9 @@ class ProfileFollowsViewModel @Inject constructor( setErrorState(error = UiState.FollowsError.MissingRelaysConfiguration(error)) updateStateProfileFollow(profileId) } catch (error: ProfileRepository.FollowListNotFound) { - Timber.w(error) - setErrorState(error = UiState.FollowsError.FailedToUnfollowUser(error)) - updateStateProfileFollow(profileId) - } catch (error: ProfileRepository.PossibleFollowListCorruption) { Timber.w(error) updateStateProfileFollow(profileId) - setState { copy(shouldApproveUnfollow = true) } + setState { copy(shouldApproveProfileAction = ProfileAction.Unfollow(profileId = profileId)) } } } 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 65a3b87d1..13bf342a9 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 @@ -110,7 +110,7 @@ class ProfileRepository @Inject constructor( } } - @Throws(FollowListNotFound::class, NostrPublishException::class, PossibleFollowListCorruption::class) + @Throws(FollowListNotFound::class, NostrPublishException::class) suspend fun follow( userId: String, followedUserId: String, @@ -121,7 +121,7 @@ class ProfileRepository @Inject constructor( } } - @Throws(FollowListNotFound::class, NostrPublishException::class, PossibleFollowListCorruption::class) + @Throws(FollowListNotFound::class, NostrPublishException::class) suspend fun unfollow( userId: String, unfollowedUserId: String, @@ -132,25 +132,27 @@ class ProfileRepository @Inject constructor( } } - @Throws(FollowListNotFound::class, NostrPublishException::class, PossibleFollowListCorruption::class) + @Throws(FollowListNotFound::class, NostrPublishException::class) private suspend fun updateFollowList( userId: String, forceUpdate: Boolean, reducer: Set.() -> Set, ) = withContext(dispatchers.io()) { val userFollowList = userAccountFetcher.fetchUserFollowListOrNull(userId = userId) - ?: throw FollowListNotFound() - - if (userFollowList.following.isEmpty() && !forceUpdate) { - throw PossibleFollowListCorruption() + val isEmptyFollowList = userFollowList == null || userFollowList.following.isEmpty() + if (isEmptyFollowList && !forceUpdate) { + throw FollowListNotFound() } - userRepository.updateFollowList(userId, userFollowList) + if (userFollowList != null) { + userRepository.updateFollowList(userId, userFollowList) + } + val existingFollowing = userFollowList?.following ?: emptySet() setFollowList( userId = userId, - contacts = userFollowList.following.reducer(), - content = userFollowList.followListEventContent ?: "", + contacts = existingFollowing.reducer(), + content = userFollowList?.followListEventContent ?: "", ) } @@ -266,5 +268,4 @@ class ProfileRepository @Inject constructor( } class FollowListNotFound : Exception() - class PossibleFollowListCorruption : Exception() } From e2b135087a69ee89e296ae7bccf4580b88b43c45 Mon Sep 17 00:00:00 2001 From: Aleksandar Ilic Date: Fri, 29 Nov 2024 22:24:38 +0100 Subject: [PATCH 5/7] Improve naming and approve alert dialog fun signature --- ...ApproveFollowUnfollowProfileAlertDialog.kt | 24 ++++++++++++------- .../explore/home/people/ExplorePeople.kt | 23 +++++++++--------- .../home/people/ExplorePeopleContract.kt | 4 ++-- .../home/people/ExplorePeopleViewModel.kt | 10 +++++--- .../profile/details/ProfileDetailsContract.kt | 4 ++-- .../details/ProfileDetailsViewModel.kt | 10 +++++--- .../details/ui/ProfileDetailsScreen.kt | 23 +++++++++--------- .../profile/follows/ProfileFollowsContract.kt | 4 ++-- .../profile/follows/ProfileFollowsScreen.kt | 23 +++++++++--------- .../follows/ProfileFollowsViewModel.kt | 6 ++--- 10 files changed, 74 insertions(+), 57 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/core/compose/profile/approvals/ApproveFollowUnfollowProfileAlertDialog.kt b/app/src/main/kotlin/net/primal/android/core/compose/profile/approvals/ApproveFollowUnfollowProfileAlertDialog.kt index 52dc1fd45..3e4f6ed29 100644 --- a/app/src/main/kotlin/net/primal/android/core/compose/profile/approvals/ApproveFollowUnfollowProfileAlertDialog.kt +++ b/app/src/main/kotlin/net/primal/android/core/compose/profile/approvals/ApproveFollowUnfollowProfileAlertDialog.kt @@ -10,12 +10,13 @@ import net.primal.android.theme.AppTheme @Composable fun ApproveFollowUnfollowProfileAlertDialog( - profileAction: ProfileAction, - onActionApproved: () -> Unit, + profileApproval: ProfileApproval, + onFollowApproved: () -> Unit, + onUnfollowApproved: () -> Unit, onClose: () -> Unit, ) { - val messages = when (profileAction) { - is ProfileAction.Follow -> { + val messages = when (profileApproval) { + is ProfileApproval.Follow -> { ApprovalMessages( title = stringResource(id = R.string.context_confirm_follow_title), text = stringResource(id = R.string.context_confirm_follow_text), @@ -24,7 +25,7 @@ fun ApproveFollowUnfollowProfileAlertDialog( ) } - is ProfileAction.Unfollow -> { + is ProfileApproval.Unfollow -> { ApprovalMessages( title = stringResource(id = R.string.context_confirm_unfollow_title), text = stringResource(id = R.string.context_confirm_unfollow_text), @@ -54,7 +55,12 @@ fun ApproveFollowUnfollowProfileAlertDialog( } }, confirmButton = { - TextButton(onClick = onActionApproved) { + TextButton( + onClick = when (profileApproval) { + is ProfileApproval.Follow -> onFollowApproved + is ProfileApproval.Unfollow -> onUnfollowApproved + }, + ) { Text( text = messages.positive, ) @@ -70,7 +76,7 @@ private data class ApprovalMessages( val negative: String, ) -sealed class ProfileAction(open val profileId: String) { - data class Follow(override val profileId: String) : ProfileAction(profileId) - data class Unfollow(override val profileId: String) : ProfileAction(profileId) +sealed class ProfileApproval(open val profileId: String) { + data class Follow(override val profileId: String) : ProfileApproval(profileId) + data class Unfollow(override val profileId: String) : ProfileApproval(profileId) } diff --git a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt index eb4266c6a..2622f79fe 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeople.kt @@ -43,7 +43,6 @@ import net.primal.android.core.compose.NostrUserText import net.primal.android.core.compose.button.FollowUnfollowButton import net.primal.android.core.compose.preview.PrimalPreview import net.primal.android.core.compose.profile.approvals.ApproveFollowUnfollowProfileAlertDialog -import net.primal.android.core.compose.profile.approvals.ProfileAction import net.primal.android.core.compose.profile.model.ProfileDetailsUi import net.primal.android.core.errors.UiError import net.primal.android.core.utils.shortened @@ -85,20 +84,22 @@ fun ExplorePeople( ) { if (state.shouldApproveProfileAction != null) { ApproveFollowUnfollowProfileAlertDialog( - profileAction = state.shouldApproveProfileAction, - onActionApproved = { - val event = when (state.shouldApproveProfileAction) { - is ProfileAction.Follow -> ExplorePeopleContract.UiEvent.FollowUser( + profileApproval = state.shouldApproveProfileAction, + onFollowApproved = { + eventPublisher( + ExplorePeopleContract.UiEvent.FollowUser( userId = state.shouldApproveProfileAction.profileId, forceUpdate = true, - ) - - is ProfileAction.Unfollow -> ExplorePeopleContract.UiEvent.UnfollowUser( + ), + ) + }, + onUnfollowApproved = { + eventPublisher( + ExplorePeopleContract.UiEvent.UnfollowUser( userId = state.shouldApproveProfileAction.profileId, forceUpdate = true, - ) - } - eventPublisher(event) + ), + ) }, onClose = { eventPublisher(ExplorePeopleContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, ) diff --git a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleContract.kt b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleContract.kt index 04990e994..7e6076a36 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleContract.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleContract.kt @@ -1,6 +1,6 @@ package net.primal.android.explore.home.people -import net.primal.android.core.compose.profile.approvals.ProfileAction +import net.primal.android.core.compose.profile.approvals.ProfileApproval import net.primal.android.core.errors.UiError import net.primal.android.explore.api.model.ExplorePeopleData @@ -11,7 +11,7 @@ interface ExplorePeopleContract { val people: List = emptyList(), val userFollowing: Set = emptySet(), val error: UiError? = null, - val shouldApproveProfileAction: ProfileAction? = null, + val shouldApproveProfileAction: ProfileApproval? = null, ) sealed class UiEvent { diff --git a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt index 93f55dfb1..04ec588aa 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.launch -import net.primal.android.core.compose.profile.approvals.ProfileAction +import net.primal.android.core.compose.profile.approvals.ProfileApproval import net.primal.android.core.errors.UiError import net.primal.android.explore.home.people.ExplorePeopleContract.UiEvent import net.primal.android.explore.home.people.ExplorePeopleContract.UiState @@ -107,7 +107,11 @@ class ExplorePeopleViewModel @Inject constructor( setState { copy(error = UiError.FailedToFollowUser(error)) } is ProfileRepository.FollowListNotFound -> - setState { copy(shouldApproveProfileAction = ProfileAction.Follow(profileId = profileId)) } + setState { + copy( + shouldApproveProfileAction = ProfileApproval.Follow(profileId = profileId), + ) + } is MissingRelaysException -> setState { copy(error = UiError.MissingRelaysConfiguration(error)) } @@ -141,7 +145,7 @@ class ExplorePeopleViewModel @Inject constructor( is ProfileRepository.FollowListNotFound -> setState { copy( - shouldApproveProfileAction = ProfileAction.Unfollow(profileId = profileId), + shouldApproveProfileAction = ProfileApproval.Unfollow(profileId = profileId), ) } diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsContract.kt b/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsContract.kt index 8c998bf37..a8d4310d8 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsContract.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsContract.kt @@ -1,6 +1,6 @@ package net.primal.android.profile.details -import net.primal.android.core.compose.profile.approvals.ProfileAction +import net.primal.android.core.compose.profile.approvals.ProfileApproval import net.primal.android.core.compose.profile.model.ProfileDetailsUi import net.primal.android.core.compose.profile.model.ProfileStatsUi import net.primal.android.core.errors.UiError @@ -27,7 +27,7 @@ interface ProfileDetailsContract { ProfileFeedSpec.AuthoredMedia, ), val error: ProfileError? = null, - val shouldApproveProfileAction: ProfileAction? = null, + val shouldApproveProfileAction: ProfileApproval? = null, val zapError: UiError? = null, val zappingState: ZappingState = ZappingState(), ) { diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt b/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt index f6ed80cd2..8b2e279e3 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import net.primal.android.core.compose.profile.approvals.ProfileAction +import net.primal.android.core.compose.profile.approvals.ProfileApproval import net.primal.android.core.compose.profile.model.asProfileDetailsUi import net.primal.android.core.compose.profile.model.asProfileStatsUi import net.primal.android.core.coroutines.CoroutineDispatcherProvider @@ -331,7 +331,11 @@ class ProfileDetailsViewModel @Inject constructor( } catch (error: ProfileRepository.FollowListNotFound) { Timber.w(error) updateStateProfileAsUnfollowed() - setState { copy(shouldApproveProfileAction = ProfileAction.Follow(profileId = followAction.profileId)) } + setState { + copy( + shouldApproveProfileAction = ProfileApproval.Follow(profileId = followAction.profileId), + ) + } } } @@ -361,7 +365,7 @@ class ProfileDetailsViewModel @Inject constructor( updateStateProfileAsFollowed() setState { copy( - shouldApproveProfileAction = ProfileAction.Unfollow(profileId = unfollowAction.profileId), + shouldApproveProfileAction = ProfileApproval.Unfollow(profileId = unfollowAction.profileId), ) } } diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt index c0d5346bf..11696b454 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt @@ -64,7 +64,6 @@ import net.primal.android.articles.feed.ArticleFeedList import net.primal.android.core.compose.SnackbarErrorHandler import net.primal.android.core.compose.preview.PrimalPreview import net.primal.android.core.compose.profile.approvals.ApproveFollowUnfollowProfileAlertDialog -import net.primal.android.core.compose.profile.approvals.ProfileAction import net.primal.android.core.compose.profile.model.ProfileDetailsUi import net.primal.android.core.compose.pulltorefresh.PrimalPullToRefreshBox import net.primal.android.core.compose.runtime.DisposableLifecycleObserverEffect @@ -301,20 +300,22 @@ fun ProfileDetailsScreen( if (state.shouldApproveProfileAction != null) { ApproveFollowUnfollowProfileAlertDialog( - profileAction = state.shouldApproveProfileAction, - onActionApproved = { - val event = when (state.shouldApproveProfileAction) { - is ProfileAction.Follow -> ProfileDetailsContract.UiEvent.FollowAction( + profileApproval = state.shouldApproveProfileAction, + onFollowApproved = { + eventPublisher( + ProfileDetailsContract.UiEvent.FollowAction( profileId = state.shouldApproveProfileAction.profileId, forceUpdate = true, - ) - - is ProfileAction.Unfollow -> ProfileDetailsContract.UiEvent.UnfollowAction( + ), + ) + }, + onUnfollowApproved = { + eventPublisher( + ProfileDetailsContract.UiEvent.UnfollowAction( profileId = state.shouldApproveProfileAction.profileId, forceUpdate = true, - ) - } - eventPublisher(event) + ), + ) }, onClose = { eventPublisher(ProfileDetailsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) diff --git a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsContract.kt b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsContract.kt index ae2214062..61167e111 100644 --- a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsContract.kt +++ b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsContract.kt @@ -1,6 +1,6 @@ package net.primal.android.profile.follows -import net.primal.android.core.compose.profile.approvals.ProfileAction +import net.primal.android.core.compose.profile.approvals.ProfileApproval import net.primal.android.core.compose.profile.model.UserProfileItemUi import net.primal.android.profile.domain.ProfileFollowsType @@ -13,7 +13,7 @@ interface ProfileFollowsContract { val userFollowing: Set = emptySet(), val error: FollowsError? = null, val users: List = emptyList(), - val shouldApproveProfileAction: ProfileAction? = null, + val shouldApproveProfileAction: ProfileApproval? = null, ) { sealed class FollowsError { data class FailedToFollowUser(val cause: Throwable) : FollowsError() diff --git a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt index eb820ad09..f2584ae47 100644 --- a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt @@ -26,7 +26,6 @@ import net.primal.android.core.compose.heightAdjustableLoadingLazyListPlaceholde import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.ArrowBack import net.primal.android.core.compose.profile.approvals.ApproveFollowUnfollowProfileAlertDialog -import net.primal.android.core.compose.profile.approvals.ProfileAction import net.primal.android.explore.search.ui.FollowUnfollowVisibility import net.primal.android.explore.search.ui.UserProfileListItem import net.primal.android.profile.domain.ProfileFollowsType @@ -74,20 +73,22 @@ private fun ProfileFollowsScreen( if (state.shouldApproveProfileAction != null) { ApproveFollowUnfollowProfileAlertDialog( - profileAction = state.shouldApproveProfileAction, - onActionApproved = { - val event = when (state.shouldApproveProfileAction) { - is ProfileAction.Follow -> ProfileFollowsContract.UiEvent.FollowProfile( + profileApproval = state.shouldApproveProfileAction, + onFollowApproved = { + eventPublisher( + ProfileFollowsContract.UiEvent.FollowProfile( profileId = state.shouldApproveProfileAction.profileId, forceUpdate = true, - ) - - is ProfileAction.Unfollow -> ProfileFollowsContract.UiEvent.UnfollowProfile( + ), + ) + }, + onUnfollowApproved = { + eventPublisher( + ProfileFollowsContract.UiEvent.UnfollowProfile( profileId = state.shouldApproveProfileAction.profileId, forceUpdate = true, - ) - } - eventPublisher(event) + ), + ) }, onClose = { eventPublisher(ProfileFollowsContract.UiEvent.DismissConfirmFollowUnfollowAlertDialog) }, ) diff --git a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt index ed64aad3e..a98043a38 100644 --- a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import net.primal.android.core.compose.profile.approvals.ProfileAction +import net.primal.android.core.compose.profile.approvals.ProfileApproval import net.primal.android.core.compose.profile.model.mapAsUserProfileUi import net.primal.android.core.coroutines.CoroutineDispatcherProvider import net.primal.android.core.utils.usernameUiFriendly @@ -151,7 +151,7 @@ class ProfileFollowsViewModel @Inject constructor( } catch (error: ProfileRepository.FollowListNotFound) { Timber.w(error) updateStateProfileUnfollow(profileId) - setState { copy(shouldApproveProfileAction = ProfileAction.Follow(profileId = profileId)) } + setState { copy(shouldApproveProfileAction = ProfileApproval.Follow(profileId = profileId)) } } } @@ -179,7 +179,7 @@ class ProfileFollowsViewModel @Inject constructor( } catch (error: ProfileRepository.FollowListNotFound) { Timber.w(error) updateStateProfileFollow(profileId) - setState { copy(shouldApproveProfileAction = ProfileAction.Unfollow(profileId = profileId)) } + setState { copy(shouldApproveProfileAction = ProfileApproval.Unfollow(profileId = profileId)) } } } From a82019dc993fa5e10e70ae559683a76e1aedb207 Mon Sep 17 00:00:00 2001 From: Aleksandar Ilic Date: Fri, 29 Nov 2024 22:39:51 +0100 Subject: [PATCH 6/7] Fix detekt issue --- .../profile/follows/ProfileFollowsScreen.kt | 91 +++++++++++-------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt index f2584ae47..7a1a46e05 100644 --- a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsScreen.kt @@ -71,6 +71,42 @@ private fun ProfileFollowsScreen( onErrorDismiss = { eventPublisher(ProfileFollowsContract.UiEvent.DismissError) }, ) + Scaffold( + modifier = Modifier, + topBar = { + PrimalTopAppBar( + title = state.profileName?.let { + when (state.followsType) { + ProfileFollowsType.Following -> stringResource(id = R.string.profile_following_title, it) + ProfileFollowsType.Followers -> stringResource(id = R.string.profile_followers_title, it) + } + } ?: "", + navigationIcon = PrimalIcons.ArrowBack, + onNavigationIconClick = onClose, + navigationIconContentDescription = stringResource(id = R.string.accessibility_back_button), + ) + }, + content = { paddingValues -> + ProfileFollowsContent( + state = state, + eventPublisher = eventPublisher, + paddingValues = paddingValues, + onProfileClick = onProfileClick, + ) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + ) +} + +@Composable +private fun ProfileFollowsContent( + state: ProfileFollowsContract.UiState, + eventPublisher: (ProfileFollowsContract.UiEvent) -> Unit, + paddingValues: PaddingValues, + onProfileClick: (String) -> Unit, +) { if (state.shouldApproveProfileAction != null) { ApproveFollowUnfollowProfileAlertDialog( profileApproval = state.shouldApproveProfileAction, @@ -94,48 +130,27 @@ private fun ProfileFollowsScreen( ) } - Scaffold( - modifier = Modifier, - topBar = { - PrimalTopAppBar( - title = state.profileName?.let { - when (state.followsType) { - ProfileFollowsType.Following -> stringResource(id = R.string.profile_following_title, it) - ProfileFollowsType.Followers -> stringResource(id = R.string.profile_followers_title, it) - } - } ?: "", - navigationIcon = PrimalIcons.ArrowBack, - onNavigationIconClick = onClose, - navigationIconContentDescription = stringResource(id = R.string.accessibility_back_button), + FollowsLazyColumn( + paddingValues = paddingValues, + state = state, + onProfileClick = onProfileClick, + onFollowProfileClick = { + eventPublisher( + ProfileFollowsContract.UiEvent.FollowProfile( + profileId = it, + forceUpdate = false, + ), ) }, - content = { paddingValues -> - FollowsLazyColumn( - paddingValues = paddingValues, - state = state, - onProfileClick = onProfileClick, - onFollowProfileClick = { - eventPublisher( - ProfileFollowsContract.UiEvent.FollowProfile( - profileId = it, - forceUpdate = false, - ), - ) - }, - onUnfollowProfileClick = { - eventPublisher( - ProfileFollowsContract.UiEvent.UnfollowProfile( - profileId = it, - forceUpdate = false, - ), - ) - }, - onRefreshClick = { eventPublisher(ProfileFollowsContract.UiEvent.ReloadData) }, + onUnfollowProfileClick = { + eventPublisher( + ProfileFollowsContract.UiEvent.UnfollowProfile( + profileId = it, + forceUpdate = false, + ), ) }, - snackbarHost = { - SnackbarHost(hostState = snackbarHostState) - }, + onRefreshClick = { eventPublisher(ProfileFollowsContract.UiEvent.ReloadData) }, ) } From 73f2d77b1b3e8e74159f6aa3808d8c0ac93c28f5 Mon Sep 17 00:00:00 2001 From: Aleksandar Ilic Date: Fri, 29 Nov 2024 23:10:28 +0100 Subject: [PATCH 7/7] Fix approval dialog not dismiss on confirmations --- .../home/people/ExplorePeopleViewModel.kt | 44 ++++++++++--------- .../details/ProfileDetailsViewModel.kt | 30 ++++++++----- .../follows/ProfileFollowsViewModel.kt | 38 ++++++++++------ 3 files changed, 66 insertions(+), 46 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt index 04ec588aa..c4efc467e 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/people/ExplorePeopleViewModel.kt @@ -89,7 +89,7 @@ class ExplorePeopleViewModel @Inject constructor( private fun follow(profileId: String, forceUpdate: Boolean) = viewModelScope.launch { - updateStateProfileFollow(profileId) + updateStateProfileFollowAndClearApprovalFlag(profileId) val followResult = runCatching { profileRepository.follow( @@ -102,30 +102,27 @@ class ExplorePeopleViewModel @Inject constructor( if (followResult.isFailure) { followResult.exceptionOrNull()?.let { error -> Timber.w(error) + updateStateProfileUnfollowAndClearApprovalFlag(profileId) when (error) { is WssException, is NostrPublishException -> setState { copy(error = UiError.FailedToFollowUser(error)) } - is ProfileRepository.FollowListNotFound -> - setState { - copy( - shouldApproveProfileAction = ProfileApproval.Follow(profileId = profileId), - ) - } + is ProfileRepository.FollowListNotFound -> setState { + copy(shouldApproveProfileAction = ProfileApproval.Follow(profileId = profileId)) + } is MissingRelaysException -> setState { copy(error = UiError.MissingRelaysConfiguration(error)) } else -> setState { copy(error = UiError.GenericError()) } } - updateStateProfileUnfollow(profileId) } } } private fun unfollow(profileId: String, forceUpdate: Boolean) = viewModelScope.launch { - updateStateProfileUnfollow(profileId) + updateStateProfileUnfollowAndClearApprovalFlag(profileId) val unfollowResult = runCatching { profileRepository.unfollow( @@ -136,32 +133,39 @@ class ExplorePeopleViewModel @Inject constructor( } if (unfollowResult.isFailure) { + updateStateProfileFollowAndClearApprovalFlag(profileId) unfollowResult.exceptionOrNull()?.let { error -> Timber.w(error) when (error) { is WssException, is NostrPublishException -> setState { copy(error = UiError.FailedToUnfollowUser(error)) } - is ProfileRepository.FollowListNotFound -> - setState { - copy( - shouldApproveProfileAction = ProfileApproval.Unfollow(profileId = profileId), - ) - } + is ProfileRepository.FollowListNotFound -> setState { + copy(shouldApproveProfileAction = ProfileApproval.Unfollow(profileId = profileId)) + } is MissingRelaysException -> setState { copy(error = UiError.MissingRelaysConfiguration(error)) } else -> setState { copy(error = UiError.GenericError()) } } - updateStateProfileFollow(profileId) } } } - private fun updateStateProfileUnfollow(profileId: String) = - setState { copy(userFollowing = userFollowing - profileId) } + private fun updateStateProfileUnfollowAndClearApprovalFlag(profileId: String) = + setState { + copy( + userFollowing = userFollowing - profileId, + shouldApproveProfileAction = null, + ) + } - private fun updateStateProfileFollow(profileId: String) = - setState { copy(userFollowing = userFollowing + profileId) } + private fun updateStateProfileFollowAndClearApprovalFlag(profileId: String) = + setState { + copy( + userFollowing = userFollowing + profileId, + shouldApproveProfileAction = null, + ) + } } diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt b/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt index 8b2e279e3..7d7977449 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ProfileDetailsViewModel.kt @@ -303,13 +303,19 @@ class ProfileDetailsViewModel @Inject constructor( Timber.w(error) } - private fun updateStateProfileAsFollowed() = setState { copy(isProfileFollowed = true) } + private fun updateStateProfileAsFollowedAndClearApprovalFlag() = + setState { + copy(isProfileFollowed = true, shouldApproveProfileAction = null) + } - private fun updateStateProfileAsUnfollowed() = setState { copy(isProfileFollowed = false) } + private fun updateStateProfileAsUnfollowedAndClearApprovalFlag() = + setState { + copy(isProfileFollowed = false, shouldApproveProfileAction = null) + } private fun follow(followAction: UiEvent.FollowAction) = viewModelScope.launch { - updateStateProfileAsFollowed() + updateStateProfileAsFollowedAndClearApprovalFlag() try { profileRepository.follow( userId = activeAccountStore.activeUserId(), @@ -318,19 +324,19 @@ class ProfileDetailsViewModel @Inject constructor( ) } catch (error: WssException) { Timber.w(error) - updateStateProfileAsUnfollowed() + updateStateProfileAsUnfollowedAndClearApprovalFlag() setErrorState(error = ProfileError.FailedToFollowProfile(error)) } catch (error: NostrPublishException) { Timber.w(error) - updateStateProfileAsUnfollowed() + updateStateProfileAsUnfollowedAndClearApprovalFlag() setErrorState(error = ProfileError.FailedToFollowProfile(error)) } catch (error: MissingRelaysException) { Timber.w(error) - updateStateProfileAsUnfollowed() + updateStateProfileAsUnfollowedAndClearApprovalFlag() setErrorState(error = ProfileError.MissingRelaysConfiguration(error)) } catch (error: ProfileRepository.FollowListNotFound) { Timber.w(error) - updateStateProfileAsUnfollowed() + updateStateProfileAsUnfollowedAndClearApprovalFlag() setState { copy( shouldApproveProfileAction = ProfileApproval.Follow(profileId = followAction.profileId), @@ -341,7 +347,7 @@ class ProfileDetailsViewModel @Inject constructor( private fun unfollow(unfollowAction: UiEvent.UnfollowAction) = viewModelScope.launch { - updateStateProfileAsUnfollowed() + updateStateProfileAsUnfollowedAndClearApprovalFlag() try { profileRepository.unfollow( userId = activeAccountStore.activeUserId(), @@ -350,19 +356,19 @@ class ProfileDetailsViewModel @Inject constructor( ) } catch (error: WssException) { Timber.w(error) - updateStateProfileAsFollowed() + updateStateProfileAsFollowedAndClearApprovalFlag() setErrorState(error = ProfileError.FailedToUnfollowProfile(error)) } catch (error: NostrPublishException) { Timber.w(error) - updateStateProfileAsFollowed() + updateStateProfileAsFollowedAndClearApprovalFlag() setErrorState(error = ProfileError.FailedToUnfollowProfile(error)) } catch (error: MissingRelaysException) { Timber.w(error) - updateStateProfileAsFollowed() + updateStateProfileAsFollowedAndClearApprovalFlag() setErrorState(error = ProfileError.MissingRelaysConfiguration(error)) } catch (error: ProfileRepository.FollowListNotFound) { Timber.w(error) - updateStateProfileAsFollowed() + updateStateProfileAsFollowedAndClearApprovalFlag() setState { copy( shouldApproveProfileAction = ProfileApproval.Unfollow(profileId = unfollowAction.profileId), diff --git a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt index a98043a38..48cb91058 100644 --- a/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/profile/follows/ProfileFollowsViewModel.kt @@ -119,17 +119,27 @@ class ProfileFollowsViewModel @Inject constructor( } } - private fun updateStateProfileFollow(profileId: String) { - setState { copy(userFollowing = this.userFollowing.toMutableSet().apply { add(profileId) }) } + private fun updateStateProfileFollowAndClearApprovalFlag(profileId: String) { + setState { + copy( + userFollowing = this.userFollowing.toMutableSet().apply { add(profileId) }, + shouldApproveProfileAction = null, + ) + } } - private fun updateStateProfileUnfollow(profileId: String) { - setState { copy(userFollowing = this.userFollowing.toMutableSet().apply { remove(profileId) }) } + private fun updateStateProfileUnfollowAndClearApprovalFlag(profileId: String) { + setState { + copy( + userFollowing = this.userFollowing.toMutableSet().apply { remove(profileId) }, + shouldApproveProfileAction = null, + ) + } } private fun follow(profileId: String, forceUpdate: Boolean) = viewModelScope.launch { - updateStateProfileFollow(profileId) + updateStateProfileFollowAndClearApprovalFlag(profileId) try { profileRepository.follow( userId = activeAccountStore.activeUserId(), @@ -139,25 +149,25 @@ class ProfileFollowsViewModel @Inject constructor( } catch (error: WssException) { Timber.w(error) setErrorState(error = UiState.FollowsError.FailedToFollowUser(error)) - updateStateProfileUnfollow(profileId) + updateStateProfileUnfollowAndClearApprovalFlag(profileId) } catch (error: NostrPublishException) { Timber.w(error) setErrorState(error = UiState.FollowsError.FailedToFollowUser(error)) - updateStateProfileUnfollow(profileId) + updateStateProfileUnfollowAndClearApprovalFlag(profileId) } catch (error: MissingRelaysException) { Timber.w(error) setErrorState(error = UiState.FollowsError.MissingRelaysConfiguration(error)) - updateStateProfileUnfollow(profileId) + updateStateProfileUnfollowAndClearApprovalFlag(profileId) } catch (error: ProfileRepository.FollowListNotFound) { Timber.w(error) - updateStateProfileUnfollow(profileId) + updateStateProfileUnfollowAndClearApprovalFlag(profileId) setState { copy(shouldApproveProfileAction = ProfileApproval.Follow(profileId = profileId)) } } } private fun unfollow(profileId: String, forceUpdate: Boolean) = viewModelScope.launch { - updateStateProfileUnfollow(profileId) + updateStateProfileUnfollowAndClearApprovalFlag(profileId) try { profileRepository.unfollow( userId = activeAccountStore.activeUserId(), @@ -167,18 +177,18 @@ class ProfileFollowsViewModel @Inject constructor( } catch (error: WssException) { Timber.w(error) setErrorState(error = UiState.FollowsError.FailedToUnfollowUser(error)) - updateStateProfileFollow(profileId) + updateStateProfileFollowAndClearApprovalFlag(profileId) } catch (error: NostrPublishException) { Timber.w(error) setErrorState(error = UiState.FollowsError.FailedToUnfollowUser(error)) - updateStateProfileFollow(profileId) + updateStateProfileFollowAndClearApprovalFlag(profileId) } catch (error: MissingRelaysException) { Timber.w(error) setErrorState(error = UiState.FollowsError.MissingRelaysConfiguration(error)) - updateStateProfileFollow(profileId) + updateStateProfileFollowAndClearApprovalFlag(profileId) } catch (error: ProfileRepository.FollowListNotFound) { Timber.w(error) - updateStateProfileFollow(profileId) + updateStateProfileFollowAndClearApprovalFlag(profileId) setState { copy(shouldApproveProfileAction = ProfileApproval.Unfollow(profileId = profileId)) } } }