Skip to content

Commit

Permalink
Implement follow approvals
Browse files Browse the repository at this point in the history
  • Loading branch information
markocic authored Nov 28, 2024
1 parent de32331 commit 8e1bae2
Show file tree
Hide file tree
Showing 14 changed files with 379 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -81,6 +86,14 @@ fun ExplorePeople(
eventPublisher: (ExplorePeopleContract.UiEvent) -> Unit,
onProfileClick: (String) -> Unit,
) {
var lastFollowUnfollowProfileId by rememberSaveable { mutableStateOf<String?>(null) }

ApprovalAlertDialogs(
state = state,
eventPublisher = eventPublisher,
lastFollowUnfollowProfileId = lastFollowUnfollowProfileId,
)

if (state.loading && state.people.isEmpty()) {
HeightAdjustableLoadingLazyListPlaceholder(
modifier = modifier.fillMaxSize(),
Expand Down Expand Up @@ -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,
),
)
},
)
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,21 @@ interface ExplorePeopleContract {
val people: List<ExplorePeopleData> = emptyList(),
val userFollowing: Set<String> = 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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,22 +76,25 @@ 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)

val followResult = runCatching {
profileRepository.follow(
userId = activeAccountStore.activeUserId(),
followedUserId = profileId,
forceUpdate = forceUpdate,
)
}

Expand All @@ -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),
Expand All @@ -115,14 +124,15 @@ class ExplorePeopleViewModel @Inject constructor(
}
}

private fun unfollow(profileId: String) =
private fun unfollow(profileId: String, forceUpdate: Boolean) =
viewModelScope.launch {
updateStateProfileUnfollow(profileId)

val unfollowResult = runCatching {
profileRepository.unfollow(
userId = activeAccountStore.activeUserId(),
unfollowedUserId = profileId,
forceUpdate = forceUpdate,
)
}

Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
) {
Expand All @@ -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?,
Expand All @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ class ProfileDetailsViewModel @Inject constructor(
)

UiEvent.DismissZapError -> setState { copy(zapError = null) }
UiEvent.DismissConfirmFollowUnfollowAlertDialog ->
setState { copy(shouldApproveFollow = false, shouldApproveUnfollow = false) }
}
}
}
Expand Down Expand Up @@ -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)
Expand All @@ -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) }
}
}

Expand All @@ -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)
Expand All @@ -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) }
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 8e1bae2

Please sign in to comment.