feat: 关注 / 取关用户 & 编辑资料
This commit is contained in:
parent
2d52b45ec1
commit
dabbe40331
|
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue