From c17a5a93b9c08970a64cab88bfafca2ff9236cb3 Mon Sep 17 00:00:00 2001 From: HuanCheng65 <22636177+HuanCheng65@users.noreply.github.com> Date: Sat, 22 Jul 2023 10:19:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=A0=E9=99=A4=E8=87=AA=E5=B7=B1?= =?UTF-8?q?=E7=9A=84=E8=B4=B4=E5=AD=90/=E5=9B=9E=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/huanchengfly/tieba/post/Extensions.kt | 6 + .../huanchengfly/tieba/post/MainActivityV2.kt | 3 + .../tieba/post/arch/BaseComposeActivity.kt | 2 + .../tieba/post/arch/GlobalEvent.kt | 2 + .../post/ui/page/subposts/SubPostsPage.kt | 219 +++++++++++++----- .../ui/page/subposts/SubPostsViewModel.kt | 75 ++++++ .../tieba/post/ui/page/thread/ThreadPage.kt | 125 ++++++---- .../post/ui/page/thread/ThreadViewModel.kt | 104 +++++++++ app/src/main/res/values/strings.xml | 4 + 9 files changed, 442 insertions(+), 98 deletions(-) diff --git a/app/src/main/java/com/huanchengfly/tieba/post/Extensions.kt b/app/src/main/java/com/huanchengfly/tieba/post/Extensions.kt index 286dc43d..80e192e4 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/Extensions.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/Extensions.kt @@ -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 ImmutableList.removeAt(index: Int): ImmutableList { + return this.toMutableList().apply { removeAt(index) }.toImmutableList() } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/MainActivityV2.kt b/app/src/main/java/com/huanchengfly/tieba/post/MainActivityV2.kt index d6fbebd9..627f9867 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/MainActivityV2.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/MainActivityV2.kt @@ -337,6 +337,9 @@ class MainActivityV2 : BaseComposeActivity() { ), ) val navController = rememberAnimatedNavController() + onGlobalEvent { + navController.navigateUp() + } val bottomSheetNavigator = rememberBottomSheetNavigator( animationSpec = spring(stiffness = Spring.StiffnessMediumLow), diff --git a/app/src/main/java/com/huanchengfly/tieba/post/arch/BaseComposeActivity.kt b/app/src/main/java/com/huanchengfly/tieba/post/arch/BaseComposeActivity.kt index e924e65a..22714ffa 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/arch/BaseComposeActivity.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/arch/BaseComposeActivity.kt @@ -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 diff --git a/app/src/main/java/com/huanchengfly/tieba/post/arch/GlobalEvent.kt b/app/src/main/java/com/huanchengfly/tieba/post/arch/GlobalEvent.kt index fea4ae8a..d66a20b7 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/arch/GlobalEvent.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/arch/GlobalEvent.kt @@ -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, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/subposts/SubPostsPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/subposts/SubPostsPage.kt index ec5927ec..846e9e0b 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/subposts/SubPostsPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/subposts/SubPostsPage.kt @@ -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?>(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, contentRenders: ImmutableList, 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 - 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 + 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() } + } } - } - ) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/subposts/SubPostsViewModel.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/subposts/SubPostsViewModel.kt index 5bfc1b23..8e6b3533 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/subposts/SubPostsViewModel.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/subposts/SubPostsViewModel.kt @@ -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() .flatMapConcat { it.producePartialChange() }, + intentFlow.filterIsInstance() + .flatMapConcat { it.producePartialChange() }, ) private fun SubPostsUiIntent.Load.producePartialChange(): Flow = @@ -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 = + TiebaApi.getInstance() + .delPostFlow( + forumId, + forumName, + threadId, + subPostId ?: postId, + tbs, + false, + deleteMyPost + ) + .map { + 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 { @@ -173,6 +215,7 @@ sealed interface SubPostsPartialChange : PartialChange { object Start : Load() data class Success( + val anti: ImmutableHolder, val forum: ImmutableHolder, val post: ImmutableHolder, val postContentRenders: ImmutableList, @@ -287,6 +330,37 @@ sealed interface SubPostsPartialChange : PartialChange { 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? = null, val forum: ImmutableHolder? = null, val post: ImmutableHolder? = null, val postContentRenders: ImmutableList = persistentListOf(), diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadPage.kt index 65f2ebfc..8e498892 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadPage.kt @@ -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?>(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(), + ) + } } } } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadViewModel.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadViewModel.kt index 433fb389..2bea6f4d 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadViewModel.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadViewModel.kt @@ -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() .flatMapConcat { it.producePartialChange() }, + intentFlow.filterIsInstance() + .flatMapConcat { it.producePartialChange() }, + intentFlow.filterIsInstance() + .flatMapConcat { it.producePartialChange() }, ) fun ThreadUiIntent.Init.producePartialChange(): Flow = @@ -379,6 +401,36 @@ class ThreadViewModel @Inject constructor() : ) ) } + + fun ThreadUiIntent.DeletePost.producePartialChange(): Flow = + TiebaApi.getInstance() + .delPostFlow(forumId, forumName, threadId, postId, tbs, false, deleteMyPost) + .map { + ThreadPartialChange.DeletePost.Success(postId) + } + .catch { + emit( + ThreadPartialChange.DeletePost.Failure( + it.getErrorCode(), + it.getErrorMessage() + ) + ) + } + + fun ThreadUiIntent.DeleteThread.producePartialChange(): Flow = + TiebaApi.getInstance() + .delThreadFlow(forumId, forumName, threadId, tbs, deleteMyThread, false) + .map { + 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 { @@ -858,6 +927,41 @@ sealed interface ThreadPartialChange : PartialChange { 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( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8f2b14a5..1dfa8d1f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -699,4 +699,8 @@ 回复 收藏到此楼 已收藏到本楼 + 删除失败 %s + 你确定要删除%s吗? + 本贴 + 这条回复