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.huanchengfly.tieba.post.utils.GsonUtil
import com.huanchengfly.tieba.post.utils.MD5Util
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import java.io.File
import kotlin.math.roundToInt
@ -150,4 +152,8 @@ fun pendingIntentFlagImmutable(): Int {
} else {
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()
onGlobalEvent<GlobalEvent.NavigateUp> {
navController.navigateUp()
}
val bottomSheetNavigator =
rememberBottomSheetNavigator(
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),

View File

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

View File

@ -16,6 +16,8 @@ import kotlinx.coroutines.launch
sealed interface GlobalEvent {
object AccountSwitched : GlobalEvent
object NavigateUp : GlobalEvent
data class StartSelectImages(
val id: String,
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.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
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.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.onEvent
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.theme.compose.ExtendedTheme
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.widgets.compose.Avatar
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.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.Sizes
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.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.utils.AccountUtil.LocalAccount
import com.huanchengfly.tieba.post.utils.DateTimeUtils
import com.huanchengfly.tieba.post.utils.StringUtil
import com.huanchengfly.tieba.post.utils.TiebaUtil
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
@ -149,6 +157,10 @@ internal fun SubPostsContent(
prop1 = SubPostsUiState::isLoading,
initial = false
)
val anti by viewModel.uiState.collectPartialAsState(
prop1 = SubPostsUiState::anti,
initial = null
)
val forum by viewModel.uiState.collectPartialAsState(
prop1 = SubPostsUiState::forum,
initial = null
@ -189,6 +201,51 @@ internal fun SubPostsContent(
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(
isEmpty = subPosts.isEmpty(),
isError = false,
@ -293,6 +350,7 @@ internal fun SubPostsContent(
PostCard(
postHolder = it,
contentRenders = postContentRenders,
canDelete = { it.author_id == account?.uid?.toLongOrNull() },
showSubPosts = false,
onAgree = {
val hasAgreed = it.get { agree?.hasAgree != 0 }
@ -318,8 +376,11 @@ internal fun SubPostsContent(
replyUserPortrait = it.author?.portrait,
)
)
}
)
},
) {
deleteSubPost = null
confirmDeleteDialogState.show()
}
VerticalDivider(thickness = 2.dp)
}
}
@ -347,6 +408,7 @@ internal fun SubPostsContent(
SubPostItem(
subPost = item,
contentRenders = subPostsContentRenders[index],
canDelete = { it.author_id == account?.uid?.toLongOrNull() },
onAgree = {
val hasAgreed = it.agree?.hasAgree != 0
viewModel.send(
@ -359,7 +421,7 @@ internal fun SubPostsContent(
)
)
},
onClickContent = {
onReplyClick = {
navigator.navigate(
ReplyPageDestination(
forumId = forumId,
@ -373,7 +435,11 @@ internal fun SubPostsContent(
replyUserPortrait = it.author?.portrait,
)
)
}
},
onMenuDeleteClick = {
deleteSubPost = it.wrapImmutable()
confirmDeleteDialogState.show()
},
)
}
}
@ -402,8 +468,10 @@ private fun SubPostItem(
subPost: ImmutableHolder<SubPostList>,
contentRenders: ImmutableList<PbContentRender>,
threadAuthorId: Long = 0L,
canDelete: (SubPostList) -> Boolean = { false },
onAgree: (SubPostList) -> Unit = {},
onClickContent: (SubPostList) -> Unit = {}
onReplyClick: (SubPostList) -> Unit = {},
onMenuDeleteClick: ((SubPostList) -> Unit)? = null,
) {
val context = LocalContext.current
val author = remember(subPost) { subPost.getImmutable { author } }
@ -413,64 +481,101 @@ private fun SubPostItem(
val agreeNum = remember(subPost) {
subPost.get { agree?.diffAgreeNum ?: 0L }
}
Card(
header = {
if (author.isNotNull()) {
author as ImmutableHolder<User>
UserHeader(
avatar = {
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 })
)
},
val menuState = rememberMenuState()
LongClickMenu(
menuState = menuState,
indication = null,
menuContent = {
DropdownMenuItem(
onClick = {
onReplyClick(subPost.get())
menuState.expanded = false
}
) {
Text(text = stringResource(id = R.string.btn_reply))
}
DropdownMenuItem(
onClick = {
TiebaUtil.copyText(context, contentRenders.joinToString("\n") { it.toString() })
menuState.expanded = false
}
) {
Text(text = stringResource(id = R.string.menu_copy))
}
DropdownMenuItem(
onClick = {
TiebaUtil.reportPost(context, subPost.get { id }.toString())
menuState.expanded = false
}
) {
Text(text = stringResource(id = R.string.title_report))
}
if (canDelete(subPost.get()) && onMenuDeleteClick != null) {
DropdownMenuItem(
onClick = {
UserActivity.launch(context, author.get { id }.toString())
onMenuDeleteClick(subPost.get())
menuState.expanded = false
}
) {
PostAgreeBtn(
hasAgreed = hasAgreed,
agreeNum = agreeNum,
onClick = { onAgree(subPost.get()) }
)
Text(text = stringResource(id = R.string.title_delete))
}
}
},
content = {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.padding(start = Sizes.Small + 8.dp)
.fillMaxWidth()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { onReplyClick(subPost.get()) }
) {
Card(
header = {
if (author.isNotNull()) {
author as ImmutableHolder<User>
UserHeader(
avatar = {
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 com.huanchengfly.tieba.post.api.TiebaApi
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.SimpleForum
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.renders
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.ImmutableHolder
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.UiState
import com.huanchengfly.tieba.post.arch.wrapImmutable
import com.huanchengfly.tieba.post.removeAt
import com.huanchengfly.tieba.post.ui.common.PbContentRender
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
@ -58,6 +63,8 @@ class SubPostsViewModel @Inject constructor() :
.flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<SubPostsUiIntent.Agree>()
.flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<SubPostsUiIntent.DeletePost>()
.flatMapConcat { it.producePartialChange() },
)
private fun SubPostsUiIntent.Load.producePartialChange(): Flow<SubPostsPartialChange.Load> =
@ -67,8 +74,10 @@ class SubPostsViewModel @Inject constructor() :
val post = checkNotNull(response.data_?.post)
val page = checkNotNull(response.data_?.page)
val forum = checkNotNull(response.data_?.forum)
val anti = checkNotNull(response.data_?.anti)
val subPosts = response.data_?.subpost_list.orEmpty()
SubPostsPartialChange.Load.Success(
anti.wrapImmutable(),
forum.wrapImmutable(),
post.wrapImmutable(),
post.contentRenders,
@ -115,6 +124,29 @@ class SubPostsViewModel @Inject constructor() :
}
.onStart { emit(SubPostsPartialChange.Agree.Start(subPostId, agree)) }
.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 agree: Boolean
) : 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> {
@ -173,6 +215,7 @@ sealed interface SubPostsPartialChange : PartialChange<SubPostsUiState> {
object Start : Load()
data class Success(
val anti: ImmutableHolder<Anti>,
val forum: ImmutableHolder<SimpleForum>,
val post: ImmutableHolder<Post>,
val postContentRenders: ImmutableList<PbContentRender>,
@ -287,6 +330,37 @@ sealed interface SubPostsPartialChange : PartialChange<SubPostsUiState> {
val throwable: Throwable,
) : 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(
@ -298,6 +372,7 @@ data class SubPostsUiState(
val totalPage: Int = 1,
val totalCount: Int = 0,
val anti: ImmutableHolder<Anti>? = null,
val forum: ImmutableHolder<SimpleForum>? = null,
val post: ImmutableHolder<Post>? = null,
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.ChromeReaderMode
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.FaceRetouchingOff
import androidx.compose.material.icons.rounded.Favorite
@ -534,6 +535,8 @@ fun ThreadPage(
val curForumId = remember(forumId, forum) {
forumId ?: forum?.get { id }
}
val curForumName = remember(forum) { forum?.get { name } }
val curTbs = remember(anti) { anti?.get { tbs } }
var waitLoadSuccessAndScrollToFirstReply by remember { mutableStateOf(scrollToReply) }
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()
PromptDialog(
onConfirm = {
@ -766,6 +810,7 @@ fun ThreadPage(
isCollected = isCollected,
isImmersiveMode = isImmersiveMode,
isDesc = curSortType == ThreadSortType.SORT_TYPE_DESC,
canDelete = { author?.get { id } == user.get { id } },
onSeeLzClick = {
viewModel.send(
ThreadUiIntent.LoadFirstPage(
@ -856,6 +901,10 @@ fun ThreadPage(
firstPostId.toString()
)
},
onDeleteClick = {
deletePost = null
confirmDeleteDialogState.show()
},
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
@ -916,6 +965,7 @@ fun ThreadPage(
PostCard(
postHolder = firstPost!!,
contentRenders = firstPostContentRenders,
canDelete = { it.author_id == user.get { id } },
immersiveMode = isImmersiveMode,
isCollected = {
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 = {
viewModel.send(
ThreadUiIntent.AddFavorite(
@ -953,7 +991,10 @@ fun ThreadPage(
)
)
},
)
) {
deletePost = null
confirmDeleteDialogState.show()
}
VerticalDivider(
modifier = Modifier
@ -1122,12 +1163,6 @@ fun ThreadPage(
)
}
},
onMenuCopyClick = {
TiebaUtil.copyText(context, it.content.plainText)
},
onMenuReportClick = {
TiebaUtil.reportPost(context, it.id.toString())
},
onMenuFavoriteClick = {
val isPostCollected =
it.id == thread?.get { collectMarkPid.toLongOrNull() }
@ -1152,8 +1187,11 @@ fun ThreadPage(
)
}
}
}
)
},
) {
deletePost = it.wrapImmutable()
confirmDeleteDialogState.show()
}
}
if (data.isEmpty()) {
item(key = "EmptyReplyTip") {
@ -1378,9 +1416,7 @@ fun PostCard(
onAgree: () -> Unit = {},
onReplyClick: (Post) -> Unit = {},
onOpenSubPosts: (subPostId: Long) -> Unit = {},
onMenuCopyClick: ((Post) -> Unit)? = null,
onMenuFavoriteClick: ((Post) -> Unit)? = null,
onMenuReportClick: ((Post) -> Unit)? = null,
onMenuDeleteClick: ((Post) -> Unit)? = null,
) {
val context = LocalContext.current
@ -1438,15 +1474,21 @@ fun PostCard(
) {
Text(text = stringResource(id = R.string.btn_reply))
}
if (onMenuCopyClick != null) {
DropdownMenuItem(
onClick = {
onMenuCopyClick(post)
menuState.expanded = false
}
) {
Text(text = stringResource(id = R.string.menu_copy))
DropdownMenuItem(
onClick = {
TiebaUtil.copyText(context, post.content.plainText)
menuState.expanded = false
}
) {
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) {
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) {
DropdownMenuItem(
onClick = {
@ -1660,6 +1692,7 @@ private fun ThreadMenu(
isCollected: Boolean,
isImmersiveMode: Boolean,
isDesc: Boolean,
canDelete: () -> Boolean,
onSeeLzClick: () -> Unit,
onCollectClick: () -> Unit,
onImmersiveModeClick: () -> Unit,
@ -1668,6 +1701,7 @@ private fun ThreadMenu(
onShareClick: () -> Unit,
onCopyLinkClick: () -> Unit,
onReportClick: () -> Unit,
onDeleteClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@ -1802,6 +1836,15 @@ private fun ThreadMenu(
onClick = onReportClick,
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.Stable
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.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.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.getErrorMessage
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.PartialChange
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.UiState
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.ui.common.PbContentRender
import com.huanchengfly.tieba.post.utils.BlockManager.shouldBlock
@ -78,6 +83,19 @@ class ThreadViewModel @Inject constructor() :
ThreadPartialChange.RemoveFavorite.Success -> ThreadUiEvent.RemoveFavoriteSuccess
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
}
}
@ -107,6 +125,10 @@ class ThreadViewModel @Inject constructor() :
.flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<ThreadUiIntent.AgreePost>()
.flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<ThreadUiIntent.DeletePost>()
.flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<ThreadUiIntent.DeleteThread>()
.flatMapConcat { it.producePartialChange() },
)
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 agree: Boolean
) : 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> {
@ -858,6 +927,41 @@ sealed interface ThreadPartialChange : PartialChange<ThreadUiState> {
val errorMessage: String
) : 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(

View File

@ -699,4 +699,8 @@
<string name="btn_reply">回复</string>
<string name="title_collect_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>