feat: 删除自己的贴子/回复

This commit is contained in:
HuanCheng65 2023-07-22 10:19:22 +08:00
parent 81550e99e5
commit c17a5a93b9
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
9 changed files with 442 additions and 98 deletions

View File

@ -21,6 +21,8 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.huanchengfly.tieba.post.utils.GsonUtil import com.huanchengfly.tieba.post.utils.GsonUtil
import com.huanchengfly.tieba.post.utils.MD5Util import com.huanchengfly.tieba.post.utils.MD5Util
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import java.io.File import java.io.File
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -150,4 +152,8 @@ fun pendingIntentFlagImmutable(): Int {
} else { } else {
0 0
} }
}
fun <T> ImmutableList<T>.removeAt(index: Int): ImmutableList<T> {
return this.toMutableList().apply { removeAt(index) }.toImmutableList()
} }

View File

@ -337,6 +337,9 @@ class MainActivityV2 : BaseComposeActivity() {
), ),
) )
val navController = rememberAnimatedNavController() val navController = rememberAnimatedNavController()
onGlobalEvent<GlobalEvent.NavigateUp> {
navController.navigateUp()
}
val bottomSheetNavigator = val bottomSheetNavigator =
rememberBottomSheetNavigator( rememberBottomSheetNavigator(
animationSpec = spring(stiffness = Spring.StiffnessMediumLow), animationSpec = spring(stiffness = Spring.StiffnessMediumLow),

View File

@ -123,6 +123,8 @@ abstract class BaseComposeActivity : BaseActivity() {
sealed interface CommonUiEvent : UiEvent { sealed interface CommonUiEvent : UiEvent {
object ScrollToTop : CommonUiEvent object ScrollToTop : CommonUiEvent
object NavigateUp : CommonUiEvent
data class Toast( data class Toast(
val message: CharSequence, val message: CharSequence,
val length: Int = android.widget.Toast.LENGTH_SHORT val length: Int = android.widget.Toast.LENGTH_SHORT

View File

@ -16,6 +16,8 @@ import kotlinx.coroutines.launch
sealed interface GlobalEvent { sealed interface GlobalEvent {
object AccountSwitched : GlobalEvent object AccountSwitched : GlobalEvent
object NavigateUp : GlobalEvent
data class StartSelectImages( data class StartSelectImages(
val id: String, val id: String,
val maxCount: Int, val maxCount: Int,

View File

@ -3,7 +3,6 @@ package com.huanchengfly.tieba.post.ui.page.subposts
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -15,6 +14,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
@ -24,7 +24,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Close
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -41,6 +43,7 @@ import com.huanchengfly.tieba.post.arch.ImmutableHolder
import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.onEvent import com.huanchengfly.tieba.post.arch.onEvent
import com.huanchengfly.tieba.post.arch.pageViewModel import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.arch.wrapImmutable
import com.huanchengfly.tieba.post.ui.common.PbContentRender import com.huanchengfly.tieba.post.ui.common.PbContentRender
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.LocalNavigator import com.huanchengfly.tieba.post.ui.page.LocalNavigator
@ -51,17 +54,22 @@ import com.huanchengfly.tieba.post.ui.page.thread.PostCard
import com.huanchengfly.tieba.post.ui.page.thread.UserNameText import com.huanchengfly.tieba.post.ui.page.thread.UserNameText
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
import com.huanchengfly.tieba.post.ui.widgets.compose.Card import com.huanchengfly.tieba.post.ui.widgets.compose.Card
import com.huanchengfly.tieba.post.ui.widgets.compose.ConfirmDialog
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
import com.huanchengfly.tieba.post.ui.widgets.compose.LongClickMenu
import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold
import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes
import com.huanchengfly.tieba.post.ui.widgets.compose.TitleCentredToolbar import com.huanchengfly.tieba.post.ui.widgets.compose.TitleCentredToolbar
import com.huanchengfly.tieba.post.ui.widgets.compose.UserHeader import com.huanchengfly.tieba.post.ui.widgets.compose.UserHeader
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberDialogState
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberMenuState
import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen
import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount
import com.huanchengfly.tieba.post.utils.DateTimeUtils import com.huanchengfly.tieba.post.utils.DateTimeUtils
import com.huanchengfly.tieba.post.utils.StringUtil import com.huanchengfly.tieba.post.utils.StringUtil
import com.huanchengfly.tieba.post.utils.TiebaUtil
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle import com.ramcosta.composedestinations.spec.DestinationStyle
@ -149,6 +157,10 @@ internal fun SubPostsContent(
prop1 = SubPostsUiState::isLoading, prop1 = SubPostsUiState::isLoading,
initial = false initial = false
) )
val anti by viewModel.uiState.collectPartialAsState(
prop1 = SubPostsUiState::anti,
initial = null
)
val forum by viewModel.uiState.collectPartialAsState( val forum by viewModel.uiState.collectPartialAsState(
prop1 = SubPostsUiState::forum, prop1 = SubPostsUiState::forum,
initial = null initial = null
@ -189,6 +201,51 @@ internal fun SubPostsContent(
lazyListState.scrollToItem(2 + subPosts.indexOfFirst { it.get { id } == subPostId }) lazyListState.scrollToItem(2 + subPosts.indexOfFirst { it.get { id } == subPostId })
} }
val confirmDeleteDialogState = rememberDialogState()
var deleteSubPost by remember { mutableStateOf<ImmutableHolder<SubPostList>?>(null) }
ConfirmDialog(
dialogState = confirmDeleteDialogState,
onConfirm = {
if (deleteSubPost == null) {
val isSelfPost = post?.get { author_id } == account?.uid?.toLongOrNull()
viewModel.send(
SubPostsUiIntent.DeletePost(
forumId = forumId,
forumName = forum?.get { name }.orEmpty(),
threadId = threadId,
postId = postId,
deleteMyPost = isSelfPost,
tbs = anti?.get { tbs },
)
)
} else {
val isSelfSubPost =
deleteSubPost!!.get { author_id } == account?.uid?.toLongOrNull()
viewModel.send(
SubPostsUiIntent.DeletePost(
forumId = forumId,
forumName = forum?.get { name }.orEmpty(),
threadId = threadId,
postId = postId,
subPostId = deleteSubPost!!.get { id },
deleteMyPost = isSelfSubPost,
tbs = anti?.get { tbs },
)
)
}
}
) {
Text(
text = stringResource(
id = R.string.message_confirm_delete,
if (deleteSubPost == null && post != null) stringResource(
id = R.string.tip_post_floor,
post!!.get { floor })
else stringResource(id = R.string.this_reply)
)
)
}
StateScreen( StateScreen(
isEmpty = subPosts.isEmpty(), isEmpty = subPosts.isEmpty(),
isError = false, isError = false,
@ -293,6 +350,7 @@ internal fun SubPostsContent(
PostCard( PostCard(
postHolder = it, postHolder = it,
contentRenders = postContentRenders, contentRenders = postContentRenders,
canDelete = { it.author_id == account?.uid?.toLongOrNull() },
showSubPosts = false, showSubPosts = false,
onAgree = { onAgree = {
val hasAgreed = it.get { agree?.hasAgree != 0 } val hasAgreed = it.get { agree?.hasAgree != 0 }
@ -318,8 +376,11 @@ internal fun SubPostsContent(
replyUserPortrait = it.author?.portrait, replyUserPortrait = it.author?.portrait,
) )
) )
} },
) ) {
deleteSubPost = null
confirmDeleteDialogState.show()
}
VerticalDivider(thickness = 2.dp) VerticalDivider(thickness = 2.dp)
} }
} }
@ -347,6 +408,7 @@ internal fun SubPostsContent(
SubPostItem( SubPostItem(
subPost = item, subPost = item,
contentRenders = subPostsContentRenders[index], contentRenders = subPostsContentRenders[index],
canDelete = { it.author_id == account?.uid?.toLongOrNull() },
onAgree = { onAgree = {
val hasAgreed = it.agree?.hasAgree != 0 val hasAgreed = it.agree?.hasAgree != 0
viewModel.send( viewModel.send(
@ -359,7 +421,7 @@ internal fun SubPostsContent(
) )
) )
}, },
onClickContent = { onReplyClick = {
navigator.navigate( navigator.navigate(
ReplyPageDestination( ReplyPageDestination(
forumId = forumId, forumId = forumId,
@ -373,7 +435,11 @@ internal fun SubPostsContent(
replyUserPortrait = it.author?.portrait, replyUserPortrait = it.author?.portrait,
) )
) )
} },
onMenuDeleteClick = {
deleteSubPost = it.wrapImmutable()
confirmDeleteDialogState.show()
},
) )
} }
} }
@ -402,8 +468,10 @@ private fun SubPostItem(
subPost: ImmutableHolder<SubPostList>, subPost: ImmutableHolder<SubPostList>,
contentRenders: ImmutableList<PbContentRender>, contentRenders: ImmutableList<PbContentRender>,
threadAuthorId: Long = 0L, threadAuthorId: Long = 0L,
canDelete: (SubPostList) -> Boolean = { false },
onAgree: (SubPostList) -> Unit = {}, onAgree: (SubPostList) -> Unit = {},
onClickContent: (SubPostList) -> Unit = {} onReplyClick: (SubPostList) -> Unit = {},
onMenuDeleteClick: ((SubPostList) -> Unit)? = null,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val author = remember(subPost) { subPost.getImmutable { author } } val author = remember(subPost) { subPost.getImmutable { author } }
@ -413,64 +481,101 @@ private fun SubPostItem(
val agreeNum = remember(subPost) { val agreeNum = remember(subPost) {
subPost.get { agree?.diffAgreeNum ?: 0L } subPost.get { agree?.diffAgreeNum ?: 0L }
} }
Card( val menuState = rememberMenuState()
header = { LongClickMenu(
if (author.isNotNull()) { menuState = menuState,
author as ImmutableHolder<User> indication = null,
UserHeader( menuContent = {
avatar = { DropdownMenuItem(
Avatar( onClick = {
data = StringUtil.getAvatarUrl(author.get { portrait }), onReplyClick(subPost.get())
size = Sizes.Small, menuState.expanded = false
contentDescription = null }
) ) {
}, Text(text = stringResource(id = R.string.btn_reply))
name = { }
UserNameText( DropdownMenuItem(
userName = StringUtil.getUsernameAnnotatedString( onClick = {
LocalContext.current, TiebaUtil.copyText(context, contentRenders.joinToString("\n") { it.toString() })
author.get { name }, menuState.expanded = false
author.get { nameShow } }
), ) {
userLevel = author.get { level_id }, Text(text = stringResource(id = R.string.menu_copy))
isLz = author.get { id } == threadAuthorId, }
bawuType = author.get { bawuType }, DropdownMenuItem(
) onClick = {
}, TiebaUtil.reportPost(context, subPost.get { id }.toString())
desc = { menuState.expanded = false
Text( }
text = getDescText( ) {
subPost.get { time }.toLong(), Text(text = stringResource(id = R.string.title_report))
author.get { ip_address }) }
) if (canDelete(subPost.get()) && onMenuDeleteClick != null) {
}, DropdownMenuItem(
onClick = { onClick = {
UserActivity.launch(context, author.get { id }.toString()) onMenuDeleteClick(subPost.get())
menuState.expanded = false
} }
) { ) {
PostAgreeBtn( Text(text = stringResource(id = R.string.title_delete))
hasAgreed = hasAgreed,
agreeNum = agreeNum,
onClick = { onAgree(subPost.get()) }
)
} }
} }
}, },
content = { onClick = { onReplyClick(subPost.get()) }
Column( ) {
verticalArrangement = Arrangement.spacedBy(8.dp), Card(
modifier = Modifier header = {
.padding(start = Sizes.Small + 8.dp) if (author.isNotNull()) {
.fillMaxWidth() author as ImmutableHolder<User>
.clickable( UserHeader(
interactionSource = remember { MutableInteractionSource() }, avatar = {
indication = null, Avatar(
data = StringUtil.getAvatarUrl(author.get { portrait }),
size = Sizes.Small,
contentDescription = null
)
},
name = {
UserNameText(
userName = StringUtil.getUsernameAnnotatedString(
LocalContext.current,
author.get { name },
author.get { nameShow }
),
userLevel = author.get { level_id },
isLz = author.get { id } == threadAuthorId,
bawuType = author.get { bawuType },
)
},
desc = {
Text(
text = getDescText(
subPost.get { time }.toLong(),
author.get { ip_address })
)
},
onClick = {
UserActivity.launch(context, author.get { id }.toString())
}
) { ) {
onClickContent(subPost.get()) PostAgreeBtn(
hasAgreed = hasAgreed,
agreeNum = agreeNum,
onClick = { onAgree(subPost.get()) }
)
} }
) { }
contentRenders.forEach { it.Render() } },
content = {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.padding(start = Sizes.Small + 8.dp)
.fillMaxWidth()
) {
contentRenders.forEach { it.Render() }
}
} }
} )
) }
} }

View File

@ -3,6 +3,8 @@ package com.huanchengfly.tieba.post.ui.page.subposts
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.huanchengfly.tieba.post.api.TiebaApi import com.huanchengfly.tieba.post.api.TiebaApi
import com.huanchengfly.tieba.post.api.models.AgreeBean import com.huanchengfly.tieba.post.api.models.AgreeBean
import com.huanchengfly.tieba.post.api.models.CommonResponse
import com.huanchengfly.tieba.post.api.models.protos.Anti
import com.huanchengfly.tieba.post.api.models.protos.Post import com.huanchengfly.tieba.post.api.models.protos.Post
import com.huanchengfly.tieba.post.api.models.protos.SimpleForum import com.huanchengfly.tieba.post.api.models.protos.SimpleForum
import com.huanchengfly.tieba.post.api.models.protos.SubPostList import com.huanchengfly.tieba.post.api.models.protos.SubPostList
@ -10,6 +12,8 @@ import com.huanchengfly.tieba.post.api.models.protos.contentRenders
import com.huanchengfly.tieba.post.api.models.protos.pbFloor.PbFloorResponse import com.huanchengfly.tieba.post.api.models.protos.pbFloor.PbFloorResponse
import com.huanchengfly.tieba.post.api.models.protos.renders import com.huanchengfly.tieba.post.api.models.protos.renders
import com.huanchengfly.tieba.post.api.models.protos.updateAgreeStatus import com.huanchengfly.tieba.post.api.models.protos.updateAgreeStatus
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorCode
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.ImmutableHolder import com.huanchengfly.tieba.post.arch.ImmutableHolder
import com.huanchengfly.tieba.post.arch.PartialChange import com.huanchengfly.tieba.post.arch.PartialChange
@ -18,6 +22,7 @@ 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.wrapImmutable import com.huanchengfly.tieba.post.arch.wrapImmutable
import com.huanchengfly.tieba.post.removeAt
import com.huanchengfly.tieba.post.ui.common.PbContentRender import com.huanchengfly.tieba.post.ui.common.PbContentRender
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -58,6 +63,8 @@ class SubPostsViewModel @Inject constructor() :
.flatMapConcat { it.producePartialChange() }, .flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<SubPostsUiIntent.Agree>() intentFlow.filterIsInstance<SubPostsUiIntent.Agree>()
.flatMapConcat { it.producePartialChange() }, .flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<SubPostsUiIntent.DeletePost>()
.flatMapConcat { it.producePartialChange() },
) )
private fun SubPostsUiIntent.Load.producePartialChange(): Flow<SubPostsPartialChange.Load> = private fun SubPostsUiIntent.Load.producePartialChange(): Flow<SubPostsPartialChange.Load> =
@ -67,8 +74,10 @@ class SubPostsViewModel @Inject constructor() :
val post = checkNotNull(response.data_?.post) val post = checkNotNull(response.data_?.post)
val page = checkNotNull(response.data_?.page) val page = checkNotNull(response.data_?.page)
val forum = checkNotNull(response.data_?.forum) val forum = checkNotNull(response.data_?.forum)
val anti = checkNotNull(response.data_?.anti)
val subPosts = response.data_?.subpost_list.orEmpty() val subPosts = response.data_?.subpost_list.orEmpty()
SubPostsPartialChange.Load.Success( SubPostsPartialChange.Load.Success(
anti.wrapImmutable(),
forum.wrapImmutable(), forum.wrapImmutable(),
post.wrapImmutable(), post.wrapImmutable(),
post.contentRenders, post.contentRenders,
@ -115,6 +124,29 @@ class SubPostsViewModel @Inject constructor() :
} }
.onStart { emit(SubPostsPartialChange.Agree.Start(subPostId, agree)) } .onStart { emit(SubPostsPartialChange.Agree.Start(subPostId, agree)) }
.catch { emit(SubPostsPartialChange.Agree.Failure(subPostId, !agree, it)) } .catch { emit(SubPostsPartialChange.Agree.Failure(subPostId, !agree, it)) }
fun SubPostsUiIntent.DeletePost.producePartialChange(): Flow<SubPostsPartialChange.DeletePost> =
TiebaApi.getInstance()
.delPostFlow(
forumId,
forumName,
threadId,
subPostId ?: postId,
tbs,
false,
deleteMyPost
)
.map<CommonResponse, SubPostsPartialChange.DeletePost> {
SubPostsPartialChange.DeletePost.Success(postId, subPostId)
}
.catch {
emit(
SubPostsPartialChange.DeletePost.Failure(
it.getErrorCode(),
it.getErrorMessage()
)
)
}
} }
} }
@ -143,6 +175,16 @@ sealed interface SubPostsUiIntent : UiIntent {
val subPostId: Long? = null, val subPostId: Long? = null,
val agree: Boolean val agree: Boolean
) : SubPostsUiIntent ) : SubPostsUiIntent
data class DeletePost(
val forumId: Long,
val forumName: String,
val threadId: Long,
val postId: Long,
val subPostId: Long? = null,
val deleteMyPost: Boolean,
val tbs: String? = null
) : SubPostsUiIntent
} }
sealed interface SubPostsPartialChange : PartialChange<SubPostsUiState> { sealed interface SubPostsPartialChange : PartialChange<SubPostsUiState> {
@ -173,6 +215,7 @@ sealed interface SubPostsPartialChange : PartialChange<SubPostsUiState> {
object Start : Load() object Start : Load()
data class Success( data class Success(
val anti: ImmutableHolder<Anti>,
val forum: ImmutableHolder<SimpleForum>, val forum: ImmutableHolder<SimpleForum>,
val post: ImmutableHolder<Post>, val post: ImmutableHolder<Post>,
val postContentRenders: ImmutableList<PbContentRender>, val postContentRenders: ImmutableList<PbContentRender>,
@ -287,6 +330,37 @@ sealed interface SubPostsPartialChange : PartialChange<SubPostsUiState> {
val throwable: Throwable, val throwable: Throwable,
) : Agree() ) : Agree()
} }
sealed class DeletePost : SubPostsPartialChange {
override fun reduce(oldState: SubPostsUiState): SubPostsUiState = when (this) {
is Success -> {
if (subPostId == null) {
oldState
} else {
val deletedSubPostIndex =
oldState.subPosts.indexOfFirst { it.get { id } == postId }
oldState.copy(
subPosts = oldState.subPosts.removeAt(deletedSubPostIndex),
subPostsContentRenders = oldState.subPostsContentRenders.removeAt(
deletedSubPostIndex
),
)
}
}
is Failure -> oldState
}
data class Success(
val postId: Long,
val subPostId: Long? = null,
) : DeletePost()
data class Failure(
val errorCode: Int,
val errorMessage: String
) : DeletePost()
}
} }
data class SubPostsUiState( data class SubPostsUiState(
@ -298,6 +372,7 @@ data class SubPostsUiState(
val totalPage: Int = 1, val totalPage: Int = 1,
val totalCount: Int = 0, val totalCount: Int = 0,
val anti: ImmutableHolder<Anti>? = null,
val forum: ImmutableHolder<SimpleForum>? = null, val forum: ImmutableHolder<SimpleForum>? = null,
val post: ImmutableHolder<Post>? = null, val post: ImmutableHolder<Post>? = null,
val postContentRenders: ImmutableList<PbContentRender> = persistentListOf(), val postContentRenders: ImmutableList<PbContentRender> = persistentListOf(),

View File

@ -45,6 +45,7 @@ import androidx.compose.material.icons.outlined.ChromeReaderMode
import androidx.compose.material.icons.rounded.AlignVerticalTop import androidx.compose.material.icons.rounded.AlignVerticalTop
import androidx.compose.material.icons.rounded.ChromeReaderMode import androidx.compose.material.icons.rounded.ChromeReaderMode
import androidx.compose.material.icons.rounded.ContentCopy import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Face6 import androidx.compose.material.icons.rounded.Face6
import androidx.compose.material.icons.rounded.FaceRetouchingOff import androidx.compose.material.icons.rounded.FaceRetouchingOff
import androidx.compose.material.icons.rounded.Favorite import androidx.compose.material.icons.rounded.Favorite
@ -534,6 +535,8 @@ fun ThreadPage(
val curForumId = remember(forumId, forum) { val curForumId = remember(forumId, forum) {
forumId ?: forum?.get { id } forumId ?: forum?.get { id }
} }
val curForumName = remember(forum) { forum?.get { name } }
val curTbs = remember(anti) { anti?.get { tbs } }
var waitLoadSuccessAndScrollToFirstReply by remember { mutableStateOf(scrollToReply) } var waitLoadSuccessAndScrollToFirstReply by remember { mutableStateOf(scrollToReply) }
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
@ -639,6 +642,47 @@ fun ThreadPage(
} }
} }
val confirmDeleteDialogState = rememberDialogState()
var deletePost by remember { mutableStateOf<ImmutableHolder<Post>?>(null) }
ConfirmDialog(
dialogState = confirmDeleteDialogState,
onConfirm = {
curForumId ?: return@ConfirmDialog
if (deletePost == null) {
val isSelfThread = author?.get { id } == user.get { id }
viewModel.send(
ThreadUiIntent.DeleteThread(
forumId = curForumId,
forumName = curForumName.orEmpty(),
threadId = threadId,
deleteMyThread = isSelfThread,
tbs = curTbs
)
)
} else {
val isSelfPost = deletePost!!.get { author_id } == user.get { id }
viewModel.send(
ThreadUiIntent.DeletePost(
forumId = curForumId,
forumName = curForumName.orEmpty(),
threadId = threadId,
postId = deletePost!!.get { id },
deleteMyPost = isSelfPost,
tbs = curTbs
)
)
}
}
) {
Text(
text = stringResource(
id = R.string.message_confirm_delete,
if (deletePost == null) stringResource(id = R.string.this_thread)
else stringResource(id = R.string.tip_post_floor, deletePost!!.get { floor })
)
)
}
val jumpToPageDialogState = rememberDialogState() val jumpToPageDialogState = rememberDialogState()
PromptDialog( PromptDialog(
onConfirm = { onConfirm = {
@ -766,6 +810,7 @@ fun ThreadPage(
isCollected = isCollected, isCollected = isCollected,
isImmersiveMode = isImmersiveMode, isImmersiveMode = isImmersiveMode,
isDesc = curSortType == ThreadSortType.SORT_TYPE_DESC, isDesc = curSortType == ThreadSortType.SORT_TYPE_DESC,
canDelete = { author?.get { id } == user.get { id } },
onSeeLzClick = { onSeeLzClick = {
viewModel.send( viewModel.send(
ThreadUiIntent.LoadFirstPage( ThreadUiIntent.LoadFirstPage(
@ -856,6 +901,10 @@ fun ThreadPage(
firstPostId.toString() firstPostId.toString()
) )
}, },
onDeleteClick = {
deletePost = null
confirmDeleteDialogState.show()
},
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 16.dp) .padding(vertical = 16.dp)
@ -916,6 +965,7 @@ fun ThreadPage(
PostCard( PostCard(
postHolder = firstPost!!, postHolder = firstPost!!,
contentRenders = firstPostContentRenders, contentRenders = firstPostContentRenders,
canDelete = { it.author_id == user.get { id } },
immersiveMode = isImmersiveMode, immersiveMode = isImmersiveMode,
isCollected = { isCollected = {
it.id == thread?.get { collectMarkPid } it.id == thread?.get { collectMarkPid }
@ -932,18 +982,6 @@ fun ThreadPage(
) )
) )
}, },
onMenuCopyClick = {
TiebaUtil.copyText(
context,
it.content.plainText
)
},
onMenuReportClick = {
TiebaUtil.reportPost(
context,
it.id.toString()
)
},
onMenuFavoriteClick = { onMenuFavoriteClick = {
viewModel.send( viewModel.send(
ThreadUiIntent.AddFavorite( ThreadUiIntent.AddFavorite(
@ -953,7 +991,10 @@ fun ThreadPage(
) )
) )
}, },
) ) {
deletePost = null
confirmDeleteDialogState.show()
}
VerticalDivider( VerticalDivider(
modifier = Modifier modifier = Modifier
@ -1122,12 +1163,6 @@ fun ThreadPage(
) )
} }
}, },
onMenuCopyClick = {
TiebaUtil.copyText(context, it.content.plainText)
},
onMenuReportClick = {
TiebaUtil.reportPost(context, it.id.toString())
},
onMenuFavoriteClick = { onMenuFavoriteClick = {
val isPostCollected = val isPostCollected =
it.id == thread?.get { collectMarkPid.toLongOrNull() } it.id == thread?.get { collectMarkPid.toLongOrNull() }
@ -1152,8 +1187,11 @@ fun ThreadPage(
) )
} }
} }
} },
) ) {
deletePost = it.wrapImmutable()
confirmDeleteDialogState.show()
}
} }
if (data.isEmpty()) { if (data.isEmpty()) {
item(key = "EmptyReplyTip") { item(key = "EmptyReplyTip") {
@ -1378,9 +1416,7 @@ fun PostCard(
onAgree: () -> Unit = {}, onAgree: () -> Unit = {},
onReplyClick: (Post) -> Unit = {}, onReplyClick: (Post) -> Unit = {},
onOpenSubPosts: (subPostId: Long) -> Unit = {}, onOpenSubPosts: (subPostId: Long) -> Unit = {},
onMenuCopyClick: ((Post) -> Unit)? = null,
onMenuFavoriteClick: ((Post) -> Unit)? = null, onMenuFavoriteClick: ((Post) -> Unit)? = null,
onMenuReportClick: ((Post) -> Unit)? = null,
onMenuDeleteClick: ((Post) -> Unit)? = null, onMenuDeleteClick: ((Post) -> Unit)? = null,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -1438,15 +1474,21 @@ fun PostCard(
) { ) {
Text(text = stringResource(id = R.string.btn_reply)) Text(text = stringResource(id = R.string.btn_reply))
} }
if (onMenuCopyClick != null) { DropdownMenuItem(
DropdownMenuItem( onClick = {
onClick = { TiebaUtil.copyText(context, post.content.plainText)
onMenuCopyClick(post) menuState.expanded = false
menuState.expanded = false
}
) {
Text(text = stringResource(id = R.string.menu_copy))
} }
) {
Text(text = stringResource(id = R.string.menu_copy))
}
DropdownMenuItem(
onClick = {
TiebaUtil.reportPost(context, post.id.toString())
menuState.expanded = false
}
) {
Text(text = stringResource(id = R.string.title_report))
} }
if (onMenuFavoriteClick != null) { if (onMenuFavoriteClick != null) {
DropdownMenuItem( DropdownMenuItem(
@ -1462,16 +1504,6 @@ fun PostCard(
} }
} }
} }
if (onMenuReportClick != null) {
DropdownMenuItem(
onClick = {
onMenuReportClick(post)
menuState.expanded = false
}
) {
Text(text = stringResource(id = R.string.title_report))
}
}
if (canDelete(post) && onMenuDeleteClick != null) { if (canDelete(post) && onMenuDeleteClick != null) {
DropdownMenuItem( DropdownMenuItem(
onClick = { onClick = {
@ -1660,6 +1692,7 @@ private fun ThreadMenu(
isCollected: Boolean, isCollected: Boolean,
isImmersiveMode: Boolean, isImmersiveMode: Boolean,
isDesc: Boolean, isDesc: Boolean,
canDelete: () -> Boolean,
onSeeLzClick: () -> Unit, onSeeLzClick: () -> Unit,
onCollectClick: () -> Unit, onCollectClick: () -> Unit,
onImmersiveModeClick: () -> Unit, onImmersiveModeClick: () -> Unit,
@ -1668,6 +1701,7 @@ private fun ThreadMenu(
onShareClick: () -> Unit, onShareClick: () -> Unit,
onCopyLinkClick: () -> Unit, onCopyLinkClick: () -> Unit,
onReportClick: () -> Unit, onReportClick: () -> Unit,
onDeleteClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
@ -1802,6 +1836,15 @@ private fun ThreadMenu(
onClick = onReportClick, onClick = onReportClick,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
if (canDelete()) {
ListMenuItem(
icon = Icons.Rounded.Delete,
text = stringResource(id = R.string.title_delete),
iconColor = ExtendedTheme.colors.text,
onClick = onDeleteClick,
modifier = Modifier.fillMaxWidth(),
)
}
} }
} }
} }

View File

@ -3,8 +3,11 @@ package com.huanchengfly.tieba.post.ui.page.thread
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
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.AgreeBean import com.huanchengfly.tieba.post.api.models.AgreeBean
import com.huanchengfly.tieba.post.api.models.CommonResponse
import com.huanchengfly.tieba.post.api.models.protos.Anti import com.huanchengfly.tieba.post.api.models.protos.Anti
import com.huanchengfly.tieba.post.api.models.protos.Post import com.huanchengfly.tieba.post.api.models.protos.Post
import com.huanchengfly.tieba.post.api.models.protos.SimpleForum import com.huanchengfly.tieba.post.api.models.protos.SimpleForum
@ -20,6 +23,7 @@ import com.huanchengfly.tieba.post.api.retrofit.exception.TiebaUnknownException
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorCode import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorCode
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage 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
@ -27,6 +31,7 @@ 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.wrapImmutable import com.huanchengfly.tieba.post.arch.wrapImmutable
import com.huanchengfly.tieba.post.removeAt
import com.huanchengfly.tieba.post.repository.PbPageRepository import com.huanchengfly.tieba.post.repository.PbPageRepository
import com.huanchengfly.tieba.post.ui.common.PbContentRender import com.huanchengfly.tieba.post.ui.common.PbContentRender
import com.huanchengfly.tieba.post.utils.BlockManager.shouldBlock import com.huanchengfly.tieba.post.utils.BlockManager.shouldBlock
@ -78,6 +83,19 @@ class ThreadViewModel @Inject constructor() :
ThreadPartialChange.RemoveFavorite.Success -> ThreadUiEvent.RemoveFavoriteSuccess ThreadPartialChange.RemoveFavorite.Success -> ThreadUiEvent.RemoveFavoriteSuccess
is ThreadPartialChange.Load.Success -> ThreadUiEvent.LoadSuccess(partialChange.currentPage) is ThreadPartialChange.Load.Success -> ThreadUiEvent.LoadSuccess(partialChange.currentPage)
is ThreadPartialChange.DeletePost.Success -> CommonUiEvent.Toast(
App.INSTANCE.getString(R.string.toast_delete_success)
)
is ThreadPartialChange.DeletePost.Failure -> CommonUiEvent.Toast(
App.INSTANCE.getString(R.string.toast_delete_failure, partialChange.errorMessage)
)
is ThreadPartialChange.DeleteThread.Success -> CommonUiEvent.NavigateUp
is ThreadPartialChange.DeleteThread.Failure -> CommonUiEvent.Toast(
App.INSTANCE.getString(R.string.toast_delete_failure, partialChange.errorMessage)
)
else -> null else -> null
} }
} }
@ -107,6 +125,10 @@ class ThreadViewModel @Inject constructor() :
.flatMapConcat { it.producePartialChange() }, .flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<ThreadUiIntent.AgreePost>() intentFlow.filterIsInstance<ThreadUiIntent.AgreePost>()
.flatMapConcat { it.producePartialChange() }, .flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<ThreadUiIntent.DeletePost>()
.flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<ThreadUiIntent.DeleteThread>()
.flatMapConcat { it.producePartialChange() },
) )
fun ThreadUiIntent.Init.producePartialChange(): Flow<ThreadPartialChange.Init> = fun ThreadUiIntent.Init.producePartialChange(): Flow<ThreadPartialChange.Init> =
@ -379,6 +401,36 @@ class ThreadViewModel @Inject constructor() :
) )
) )
} }
fun ThreadUiIntent.DeletePost.producePartialChange(): Flow<ThreadPartialChange.DeletePost> =
TiebaApi.getInstance()
.delPostFlow(forumId, forumName, threadId, postId, tbs, false, deleteMyPost)
.map<CommonResponse, ThreadPartialChange.DeletePost> {
ThreadPartialChange.DeletePost.Success(postId)
}
.catch {
emit(
ThreadPartialChange.DeletePost.Failure(
it.getErrorCode(),
it.getErrorMessage()
)
)
}
fun ThreadUiIntent.DeleteThread.producePartialChange(): Flow<ThreadPartialChange.DeleteThread> =
TiebaApi.getInstance()
.delThreadFlow(forumId, forumName, threadId, tbs, deleteMyThread, false)
.map<CommonResponse, ThreadPartialChange.DeleteThread> {
ThreadPartialChange.DeleteThread.Success
}
.catch {
emit(
ThreadPartialChange.DeleteThread.Failure(
it.getErrorCode(),
it.getErrorMessage()
)
)
}
} }
} }
@ -456,6 +508,23 @@ sealed interface ThreadUiIntent : UiIntent {
val postId: Long, val postId: Long,
val agree: Boolean val agree: Boolean
) : ThreadUiIntent ) : ThreadUiIntent
data class DeletePost(
val forumId: Long,
val forumName: String,
val threadId: Long,
val postId: Long,
val deleteMyPost: Boolean,
val tbs: String? = null
) : ThreadUiIntent
data class DeleteThread(
val forumId: Long,
val forumName: String,
val threadId: Long,
val deleteMyThread: Boolean,
val tbs: String? = null
) : ThreadUiIntent
} }
sealed interface ThreadPartialChange : PartialChange<ThreadUiState> { sealed interface ThreadPartialChange : PartialChange<ThreadUiState> {
@ -858,6 +927,41 @@ sealed interface ThreadPartialChange : PartialChange<ThreadUiState> {
val errorMessage: String val errorMessage: String
) : AgreePost() ) : AgreePost()
} }
sealed class DeletePost : ThreadPartialChange {
override fun reduce(oldState: ThreadUiState): ThreadUiState = when (this) {
is Success -> {
val deletedPostIndex = oldState.data.indexOfFirst { it.post.get { id } == postId }
oldState.copy(
data = oldState.data.removeAt(deletedPostIndex),
contentRenders = oldState.contentRenders.removeAt(deletedPostIndex),
subPostContents = oldState.subPostContents.removeAt(deletedPostIndex)
)
}
is Failure -> oldState
}
data class Success(
val postId: Long
) : DeletePost()
data class Failure(
val errorCode: Int,
val errorMessage: String
) : DeletePost()
}
sealed class DeleteThread : ThreadPartialChange {
override fun reduce(oldState: ThreadUiState): ThreadUiState = oldState
object Success : DeleteThread()
data class Failure(
val errorCode: Int,
val errorMessage: String
) : DeleteThread()
}
} }
data class ThreadUiState( data class ThreadUiState(

View File

@ -699,4 +699,8 @@
<string name="btn_reply">回复</string> <string name="btn_reply">回复</string>
<string name="title_collect_floor">收藏到此楼</string> <string name="title_collect_floor">收藏到此楼</string>
<string name="title_collected_floor">已收藏到本楼</string> <string name="title_collected_floor">已收藏到本楼</string>
<string name="toast_delete_failure">删除失败 %s</string>
<string name="message_confirm_delete">你确定要删除%s吗</string>
<string name="this_thread">本贴</string>
<string name="this_reply">这条回复</string>
</resources> </resources>