feat: 关注 / 取关用户 & 编辑资料

This commit is contained in:
HuanCheng65 2023-10-08 01:03:02 +08:00
parent 2d52b45ec1
commit dabbe40331
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
2 changed files with 206 additions and 37 deletions

View File

@ -32,6 +32,7 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -67,8 +68,10 @@ import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.emitGlobalEvent import com.huanchengfly.tieba.post.arch.emitGlobalEvent
import com.huanchengfly.tieba.post.arch.getOrNull import com.huanchengfly.tieba.post.arch.getOrNull
import com.huanchengfly.tieba.post.arch.pageViewModel import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.goToActivity
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.page.ProvideNavigator import com.huanchengfly.tieba.post.ui.page.ProvideNavigator
import com.huanchengfly.tieba.post.ui.page.editprofile.view.EditProfileActivity
import com.huanchengfly.tieba.post.ui.page.user.likeforum.UserLikeForumPage import com.huanchengfly.tieba.post.ui.page.user.likeforum.UserLikeForumPage
import com.huanchengfly.tieba.post.ui.page.user.post.UserPostPage import com.huanchengfly.tieba.post.ui.page.user.post.UserPostPage
import com.huanchengfly.tieba.post.ui.widgets.Chip import com.huanchengfly.tieba.post.ui.widgets.Chip
@ -108,9 +111,14 @@ fun UserProfilePage(
navigator: DestinationsNavigator, navigator: DestinationsNavigator,
viewModel: UserProfileViewModel = pageViewModel(), viewModel: UserProfileViewModel = pageViewModel(),
) { ) {
val account = LocalAccount.current
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val density = LocalDensity.current val density = LocalDensity.current
val account = LocalAccount.current
val isSelf = remember(account, uid) {
account?.uid == uid.toString()
}
LazyLoad(loaded = viewModel.initialized) { LazyLoad(loaded = viewModel.initialized) {
viewModel.send(UserProfileUiIntent.Refresh(uid)) viewModel.send(UserProfileUiIntent.Refresh(uid))
@ -129,6 +137,10 @@ fun UserProfilePage(
prop1 = UserProfileUiState::user, prop1 = UserProfileUiState::user,
initial = null initial = null
) )
val disableButton by viewModel.uiState.collectPartialAsState(
prop1 = UserProfileUiState::disableButton,
initial = false
)
val isError by remember { val isError by remember {
derivedStateOf { error != null } derivedStateOf { error != null }
@ -260,21 +272,21 @@ fun UserProfilePage(
) )
} }
), ),
UserProfilePageData( // UserProfilePageData(
id = "posts", // id = "posts",
title = { // title = {
stringResource( // stringResource(
id = R.string.title_profile_posts_tab, // id = R.string.title_profile_posts_tab,
it.get { post_num }.getShortNumString() // it.get { post_num }.getShortNumString()
) // )
}, // },
content = { // content = {
UserPostPage( // UserPostPage(
uid = uid, // uid = uid,
isThread = false // isThread = false
) // )
} // }
).takeIf { account?.uid == uid.toString() }, // ).takeIf { account?.uid == uid.toString() },
UserProfilePageData( UserProfilePageData(
id = "concern_forums", id = "concern_forums",
title = { title = {
@ -314,7 +326,31 @@ fun UserProfilePage(
) )
.onSizeChanged { .onSizeChanged {
headerHeight = it.height.toFloat() headerHeight = it.height.toFloat()
},
showBtn = account != null,
isSelf = isSelf,
onBtnClick = {
if (disableButton || account == null) {
return@UserProfileDetail
} }
if (isSelf) {
context.goToActivity<EditProfileActivity>()
} else if (holder.get { has_concerned } == 0) {
viewModel.send(
UserProfileUiIntent.Follow(
holder.get { portrait },
account.tbs,
)
)
} else {
viewModel.send(
UserProfileUiIntent.Unfollow(
holder.get { portrait },
account.tbs,
)
)
}
}
) )
} }
@ -434,6 +470,9 @@ private fun ToolbarUserTitle(
private fun UserProfileDetail( private fun UserProfileDetail(
user: ImmutableHolder<User>, user: ImmutableHolder<User>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showBtn: Boolean = true,
isSelf: Boolean = false,
onBtnClick: () -> Unit = {},
) { ) {
Column( Column(
modifier = modifier, modifier = modifier,
@ -451,28 +490,33 @@ private fun UserProfileDetail(
modifier = Modifier.padding(bottom = 16.dp) modifier = Modifier.padding(bottom = 16.dp)
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Button( if (showBtn) {
onClick = { /*TODO*/ }, Button(
colors = if (user.get { has_concerned } == 0) { onClick = onBtnClick,
ButtonDefaults.buttonColors() colors = if (user.get { has_concerned } == 0 || isSelf) {
} else { ButtonDefaults.buttonColors()
ButtonDefaults.outlinedButtonColors()
},
border = if (user.get { has_concerned } == 0) {
null
} else {
ButtonDefaults.outlinedBorder
},
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
if (user.get { has_concerned } == 0) {
Icon(imageVector = Icons.Rounded.Add, contentDescription = null)
Text(text = stringResource(id = R.string.button_follow))
} else { } else {
Text(text = stringResource(id = R.string.button_unfollow)) ButtonDefaults.outlinedButtonColors()
},
border = if (user.get { has_concerned } == 0 || isSelf) {
null
} else {
ButtonDefaults.outlinedBorder
},
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
if (isSelf) {
Icon(imageVector = Icons.Rounded.Edit, contentDescription = null)
Text(text = stringResource(id = R.string.menu_edit_info))
} else if (user.get { has_concerned } == 0) {
Icon(imageVector = Icons.Rounded.Add, contentDescription = null)
Text(text = stringResource(id = R.string.button_follow))
} else {
Text(text = stringResource(id = R.string.button_unfollow))
}
} }
} }
} }

View File

@ -1,15 +1,22 @@
package com.huanchengfly.tieba.post.ui.page.user package com.huanchengfly.tieba.post.ui.page.user
import com.huanchengfly.tieba.post.App
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.TiebaApi import com.huanchengfly.tieba.post.api.TiebaApi
import com.huanchengfly.tieba.post.api.models.CommonResponse
import com.huanchengfly.tieba.post.api.models.FollowBean
import com.huanchengfly.tieba.post.api.models.protos.User import com.huanchengfly.tieba.post.api.models.protos.User
import com.huanchengfly.tieba.post.api.models.protos.profile.ProfileResponse import com.huanchengfly.tieba.post.api.models.protos.profile.ProfileResponse
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage
import com.huanchengfly.tieba.post.arch.BaseViewModel import com.huanchengfly.tieba.post.arch.BaseViewModel
import com.huanchengfly.tieba.post.arch.CommonUiEvent
import com.huanchengfly.tieba.post.arch.ImmutableHolder import com.huanchengfly.tieba.post.arch.ImmutableHolder
import com.huanchengfly.tieba.post.arch.PartialChange import com.huanchengfly.tieba.post.arch.PartialChange
import com.huanchengfly.tieba.post.arch.PartialChangeProducer import com.huanchengfly.tieba.post.arch.PartialChangeProducer
import com.huanchengfly.tieba.post.arch.UiEvent import com.huanchengfly.tieba.post.arch.UiEvent
import com.huanchengfly.tieba.post.arch.UiIntent import com.huanchengfly.tieba.post.arch.UiIntent
import com.huanchengfly.tieba.post.arch.UiState import com.huanchengfly.tieba.post.arch.UiState
import com.huanchengfly.tieba.post.arch.getOrNull
import com.huanchengfly.tieba.post.arch.wrapImmutable import com.huanchengfly.tieba.post.arch.wrapImmutable
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -30,6 +37,25 @@ class UserProfileViewModel @Inject constructor() :
override fun createPartialChangeProducer(): PartialChangeProducer<UserProfileUiIntent, UserProfilePartialChange, UserProfileUiState> = override fun createPartialChangeProducer(): PartialChangeProducer<UserProfileUiIntent, UserProfilePartialChange, UserProfileUiState> =
UserProfilePartialChangeProducer UserProfilePartialChangeProducer
override fun dispatchEvent(partialChange: UserProfilePartialChange): UiEvent? =
when (partialChange) {
is UserProfilePartialChange.Follow.Failure -> CommonUiEvent.Toast(
App.INSTANCE.getString(
R.string.toast_like_failed,
partialChange.error.getErrorMessage()
)
)
is UserProfilePartialChange.Unfollow.Failure -> CommonUiEvent.Toast(
App.INSTANCE.getString(
R.string.toast_unlike_failed,
partialChange.error.getErrorMessage()
)
)
else -> null
}
private object UserProfilePartialChangeProducer : private object UserProfilePartialChangeProducer :
PartialChangeProducer<UserProfileUiIntent, UserProfilePartialChange, UserProfileUiState> { PartialChangeProducer<UserProfileUiIntent, UserProfilePartialChange, UserProfileUiState> {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@ -37,6 +63,10 @@ class UserProfileViewModel @Inject constructor() :
merge( merge(
intentFlow.filterIsInstance<UserProfileUiIntent.Refresh>() intentFlow.filterIsInstance<UserProfileUiIntent.Refresh>()
.flatMapConcat { it.producePartialChange() }, .flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<UserProfileUiIntent.Follow>()
.flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<UserProfileUiIntent.Unfollow>()
.flatMapConcat { it.producePartialChange() },
) )
private fun UserProfileUiIntent.Refresh.producePartialChange(): Flow<UserProfilePartialChange.Refresh> = private fun UserProfileUiIntent.Refresh.producePartialChange(): Flow<UserProfilePartialChange.Refresh> =
@ -49,6 +79,24 @@ class UserProfileViewModel @Inject constructor() :
} }
.onStart { emit(UserProfilePartialChange.Refresh.Start) } .onStart { emit(UserProfilePartialChange.Refresh.Start) }
.catch { emit(UserProfilePartialChange.Refresh.Failure(it)) } .catch { emit(UserProfilePartialChange.Refresh.Failure(it)) }
private fun UserProfileUiIntent.Follow.producePartialChange(): Flow<UserProfilePartialChange.Follow> =
TiebaApi.getInstance()
.followFlow(portrait, tbs)
.map<FollowBean, UserProfilePartialChange.Follow> {
UserProfilePartialChange.Follow.Success
}
.onStart { emit(UserProfilePartialChange.Follow.Start) }
.catch { emit(UserProfilePartialChange.Follow.Failure(it)) }
private fun UserProfileUiIntent.Unfollow.producePartialChange(): Flow<UserProfilePartialChange.Unfollow> =
TiebaApi.getInstance()
.unfollowFlow(portrait, tbs)
.map<CommonResponse, UserProfilePartialChange.Unfollow> {
UserProfilePartialChange.Unfollow.Success
}
.onStart { emit(UserProfilePartialChange.Unfollow.Start) }
.catch { emit(UserProfilePartialChange.Unfollow.Failure(it)) }
} }
} }
@ -56,6 +104,16 @@ sealed interface UserProfileUiIntent : UiIntent {
data class Refresh( data class Refresh(
val uid: Long, val uid: Long,
) : UserProfileUiIntent ) : UserProfileUiIntent
data class Follow(
val portrait: String,
val tbs: String,
) : UserProfileUiIntent
data class Unfollow(
val portrait: String,
val tbs: String,
) : UserProfileUiIntent
} }
sealed interface UserProfilePartialChange : PartialChange<UserProfileUiState> { sealed interface UserProfilePartialChange : PartialChange<UserProfileUiState> {
@ -87,11 +145,78 @@ sealed interface UserProfilePartialChange : PartialChange<UserProfileUiState> {
val error: Throwable, val error: Throwable,
) : Refresh() ) : Refresh()
} }
sealed class Follow : UserProfilePartialChange {
override fun reduce(oldState: UserProfileUiState): UserProfileUiState = when (this) {
is Start -> oldState.copy(
user = oldState.user.getOrNull()?.copy(
has_concerned = 1
)?.wrapImmutable(),
disableButton = true
)
is Success -> oldState.copy(
user = oldState.user.getOrNull()?.copy(
has_concerned = 1
)?.wrapImmutable(),
disableButton = false
)
is Failure -> oldState.copy(
user = oldState.user.getOrNull()?.copy(
has_concerned = 0
)?.wrapImmutable(),
disableButton = false
)
}
data object Start : Follow()
data object Success : Follow()
data class Failure(
val error: Throwable,
) : Follow()
}
sealed class Unfollow : UserProfilePartialChange {
override fun reduce(oldState: UserProfileUiState): UserProfileUiState = when (this) {
is Start -> oldState.copy(
user = oldState.user.getOrNull()?.copy(
has_concerned = 0
)?.wrapImmutable(),
disableButton = true
)
is Success -> oldState.copy(
user = oldState.user.getOrNull()?.copy(
has_concerned = 0
)?.wrapImmutable(),
disableButton = false
)
is Failure -> oldState.copy(
user = oldState.user.getOrNull()?.copy(
has_concerned = 1
)?.wrapImmutable(),
disableButton = false
)
}
data object Start : Unfollow()
data object Success : Unfollow()
data class Failure(
val error: Throwable,
) : Unfollow()
}
} }
data class UserProfileUiState( data class UserProfileUiState(
val isRefreshing: Boolean = false, val isRefreshing: Boolean = false,
val error: ImmutableHolder<Throwable>? = null, val error: ImmutableHolder<Throwable>? = null,
val disableButton: Boolean = false,
val user: ImmutableHolder<User>? = null, val user: ImmutableHolder<User>? = null,
) : UiState ) : UiState