Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement follow approvals #237

Merged
merged 7 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) }
AleksandarIlic marked this conversation as resolved.
Show resolved Hide resolved

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,
AleksandarIlic marked this conversation as resolved.
Show resolved Hide resolved
)

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,
AleksandarIlic marked this conversation as resolved.
Show resolved Hide resolved
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