diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/models/protos/Extensions.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/models/protos/Extensions.kt index e444cb5b..3681bf5d 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/api/models/protos/Extensions.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/models/protos/Extensions.kt @@ -18,6 +18,7 @@ import com.huanchengfly.tieba.post.ui.common.TextContentRender.Companion.appendT import com.huanchengfly.tieba.post.ui.common.VideoContentRender import com.huanchengfly.tieba.post.ui.common.VoiceContentRender import com.huanchengfly.tieba.post.ui.common.theme.utils.ThemeUtils +import com.huanchengfly.tieba.post.ui.page.thread.SubPostItemData import com.huanchengfly.tieba.post.ui.utils.getPhotoViewData import com.huanchengfly.tieba.post.utils.EmoticonManager import com.huanchengfly.tieba.post.utils.EmoticonUtil.emoticonString @@ -330,6 +331,16 @@ val Post.subPostContents: ImmutableList ?.toImmutableList() ?: persistentListOf() +val Post.subPosts: ImmutableList + get() = sub_post_list?.sub_post_list?.map { + SubPostItemData( + it.wrapImmutable(), + it.getContentText(origin_thread_info?.author?.id) + ) + } + ?.toImmutableList() + ?: persistentListOf() + @OptIn(ExperimentalTextApi::class) fun SubPostList.getContentText(threadAuthorId: Long? = null): AnnotatedString { val context = App.INSTANCE diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/threadlist/ForumThreadListPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/threadlist/ForumThreadListPage.kt index a9f970c3..dd76ea36 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/threadlist/ForumThreadListPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/threadlist/ForumThreadListPage.kt @@ -1,7 +1,6 @@ package com.huanchengfly.tieba.post.ui.page.forum.threadlist import android.content.Context -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement @@ -56,12 +55,13 @@ import com.huanchengfly.tieba.post.ui.page.LocalNavigator import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination import com.huanchengfly.tieba.post.ui.page.forum.getSortType import com.huanchengfly.tieba.post.ui.widgets.Chip +import com.huanchengfly.tieba.post.ui.widgets.compose.BlockTip +import com.huanchengfly.tieba.post.ui.widgets.compose.BlockableContent import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard 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.LocalSnackbarHostState import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider -import com.huanchengfly.tieba.post.utils.appPreferences import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -185,70 +185,59 @@ private fun ThreadList( } } ) { index, (holder, blocked) -> - if (blocked) { - if (!LocalContext.current.appPreferences.hideBlockedContent) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 16.dp) - .clip(RoundedCornerShape(6.dp)) - .background(ExtendedTheme.colors.card) - .padding(vertical = 8.dp, horizontal = 16.dp) - ) { - Text( - text = stringResource(id = R.string.tip_blocked_thread), - style = MaterialTheme.typography.caption, - color = ExtendedTheme.colors.textSecondary - ) - } - } - return@itemsIndexed - } - val (item) = holder - Column( - modifier = Modifier.fillMaxWidth(itemFraction) + BlockableContent( + blocked = blocked, + blockedTip = { BlockTip(text = { Text(text = stringResource(id = R.string.tip_blocked_thread)) }) }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp), ) { - if (item.isTop == 1) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onItemClicked(item) + val (item) = holder + Column( + modifier = Modifier.fillMaxWidth(itemFraction) + ) { + if (item.isTop == 1) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onItemClicked(item) + } + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Chip( + text = stringResource(id = R.string.content_top), + shape = RoundedCornerShape(3.dp) + ) + var title = item.title + if (title.isBlank()) { + title = item.abstractText } - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - Chip( - text = stringResource(id = R.string.content_top), - shape = RoundedCornerShape(3.dp) - ) - var title = item.title - if (title.isBlank()) { - title = item.abstractText + Text( + text = title, + style = MaterialTheme.typography.subtitle2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + fontSize = 15.sp + ) } - Text( - text = title, - style = MaterialTheme.typography.subtitle2, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f), - fontSize = 15.sp + } else { + if (index > 0) { + if (items[index - 1].thread.get { isTop } == 1) { + Spacer(modifier = Modifier.height(8.dp)) + } + VerticalDivider(modifier = Modifier.padding(horizontal = 16.dp)) + } + FeedCard( + item = holder, + onClick = onItemClicked, + onReplyClick = onItemReplyClicked, + onAgree = onAgree, ) } - } else { - if (index > 0) { - if (items[index - 1].thread.get { isTop } == 1) { - Spacer(modifier = Modifier.height(8.dp)) - } - VerticalDivider(modifier = Modifier.padding(horizontal = 16.dp)) - } - FeedCard( - item = holder, - onClick = onItemClicked, - onReplyClick = onItemReplyClicked, - onAgree = onAgree, - ) } } } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt index cd39894b..121ddb3c 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt @@ -64,6 +64,8 @@ import com.huanchengfly.tieba.post.ui.models.ThreadItemData import com.huanchengfly.tieba.post.ui.page.LocalNavigator import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination +import com.huanchengfly.tieba.post.ui.widgets.compose.BlockTip +import com.huanchengfly.tieba.post.ui.widgets.compose.BlockableContent import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout @@ -307,7 +309,13 @@ private fun FeedList( enter = EnterTransition.None, exit = shrinkVertically() + fadeOut() ) { - if (!blocked) { + BlockableContent( + blocked = blocked, + blockedTip = { BlockTip(text = { Text(text = stringResource(id = R.string.tip_blocked_thread)) }) }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp) + ) { FeedCard( item = item, onClick = onItemClick, @@ -328,21 +336,6 @@ private fun FeedList( ) } } - } else { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 16.dp) - .clip(RoundedCornerShape(6.dp)) - .background(ExtendedTheme.colors.card) - .padding(vertical = 8.dp, horizontal = 16.dp) - ) { - Text( - text = stringResource(id = R.string.tip_blocked_thread), - style = MaterialTheme.typography.caption, - color = ExtendedTheme.colors.textSecondary - ) - } } } if (showDivider) { 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 fac0b84b..33b1ad75 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 @@ -40,14 +40,12 @@ import com.huanchengfly.tieba.post.App import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.activities.UserActivity import com.huanchengfly.tieba.post.api.models.protos.SubPostList -import com.huanchengfly.tieba.post.api.models.protos.User import com.huanchengfly.tieba.post.api.models.protos.bawuType 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 import com.huanchengfly.tieba.post.ui.page.ProvideNavigator @@ -57,6 +55,8 @@ import com.huanchengfly.tieba.post.ui.page.thread.PostAgreeBtn 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.BlockTip +import com.huanchengfly.tieba.post.ui.widgets.compose.BlockableContent 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 @@ -77,7 +77,6 @@ import com.huanchengfly.tieba.post.utils.TiebaUtil import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.spec.DestinationStyleBottomSheet -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay @@ -187,10 +186,6 @@ internal fun SubPostsContent( prop1 = SubPostsUiState::subPosts, initial = persistentListOf() ) - val subPostsContentRenders by viewModel.uiState.collectPartialAsState( - prop1 = SubPostsUiState::subPostsContentRenders, - initial = persistentListOf() - ) val currentPage by viewModel.uiState.collectPartialAsState( prop1 = SubPostsUiState::currentPage, initial = 1 @@ -208,7 +203,7 @@ internal fun SubPostsContent( viewModel.onEvent { delay(20) - lazyListState.scrollToItem(2 + subPosts.indexOfFirst { it.get { id } == subPostId }) + lazyListState.scrollToItem(2 + subPosts.indexOfFirst { it.id == subPostId }) } val confirmDeleteDialogState = rememberDialogState() @@ -432,11 +427,10 @@ internal fun SubPostsContent( } itemsIndexed( items = subPosts, - key = { _, subPost -> subPost.get { id } } + key = { _, subPost -> subPost.id } ) { index, item -> SubPostItem( - subPost = item, - contentRenders = subPostsContentRenders[index], + item = item, canDelete = { it.author_id == account?.uid?.toLongOrNull() }, threadAuthorId = thread?.get { author?.id }, onAgree = { @@ -495,16 +489,16 @@ private fun getDescText( @Composable private fun SubPostItem( - subPost: ImmutableHolder, - contentRenders: ImmutableList, + item: SubPostItemData, threadAuthorId: Long? = null, canDelete: (SubPostList) -> Boolean = { false }, onAgree: (SubPostList) -> Unit = {}, onReplyClick: (SubPostList) -> Unit = {}, onMenuDeleteClick: ((SubPostList) -> Unit)? = null, ) { + val (subPost, contentRenders, blocked) = item val context = LocalContext.current - val author = remember(subPost) { subPost.getImmutable { author } } + val author = remember(subPost) { subPost.get { author }?.wrapImmutable() } val hasAgreed = remember(subPost) { subPost.get { agree?.hasAgree == 1 } } @@ -512,100 +506,109 @@ private fun SubPostItem( subPost.get { agree?.diffAgreeNum ?: 0L } } 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) { + BlockableContent( + blocked = blocked, + blockedTip = { BlockTip(text = { Text(text = stringResource(id = R.string.tip_blocked_sub_post)) }) }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + LongClickMenu( + menuState = menuState, + indication = null, + menuContent = { DropdownMenuItem( onClick = { - onMenuDeleteClick(subPost.get()) + onReplyClick(subPost.get()) menuState.expanded = false } ) { - Text(text = stringResource(id = R.string.title_delete)) + Text(text = stringResource(id = R.string.btn_reply)) } - } - }, - 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 }) - ) - }, + 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() - ) { - contentRenders.fastForEach { it.Render() } + onClick = { onReplyClick(subPost.get()) } + ) { + Card( + header = { + if (author != null) { + 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()) + } + ) { + PostAgreeBtn( + hasAgreed = hasAgreed, + agreeNum = agreeNum, + onClick = { onAgree(subPost.get()) } + ) + } + } + }, + content = { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .padding(start = Sizes.Small + 8.dp) + .fillMaxWidth() + ) { + contentRenders.fastForEach { 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 d10dabb9..5fca3e3a 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 @@ -1,5 +1,6 @@ package com.huanchengfly.tieba.post.ui.page.subposts +import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.huanchengfly.tieba.post.api.TiebaApi import com.huanchengfly.tieba.post.api.models.AgreeBean @@ -9,6 +10,7 @@ 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 import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo +import com.huanchengfly.tieba.post.api.models.protos.User 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 @@ -23,8 +25,8 @@ 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 com.huanchengfly.tieba.post.utils.BlockManager.shouldBlock import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -77,15 +79,19 @@ class SubPostsViewModel @Inject constructor() : val forum = checkNotNull(response.data_?.forum) val thread = checkNotNull(response.data_?.thread) val anti = checkNotNull(response.data_?.anti) - val subPosts = response.data_?.subpost_list.orEmpty() + val subPosts = response.data_?.subpost_list.orEmpty().map { + SubPostItemData( + it.wrapImmutable(), + it.content.renders.toImmutableList(), + ) + }.toImmutableList() SubPostsPartialChange.Load.Success( anti.wrapImmutable(), forum.wrapImmutable(), thread.wrapImmutable(), post.wrapImmutable(), post.contentRenders, - subPosts.wrapImmutable(), - subPosts.map { it.content.renders }.toImmutableList(), + subPosts, page.current_page < page.total_page, page.current_page, page.total_page, @@ -100,10 +106,14 @@ class SubPostsViewModel @Inject constructor() : .pbFloorFlow(threadId, postId, forumId, page, subPostId) .map { response -> val page = checkNotNull(response.data_?.page) - val subPosts = response.data_?.subpost_list.orEmpty() + val subPosts = response.data_?.subpost_list.orEmpty().map { + SubPostItemData( + it.wrapImmutable(), + it.content.renders.toImmutableList(), + ) + }.toImmutableList() SubPostsPartialChange.LoadMore.Success( - subPosts.wrapImmutable(), - subPosts.map { it.content.renders }.toImmutableList(), + subPosts, page.current_page < page.total_page, page.current_page, page.total_page, @@ -208,7 +218,6 @@ sealed interface SubPostsPartialChange : PartialChange { post = post, postContentRenders = postContentRenders, subPosts = subPosts, - subPostsContentRenders = subPostsContentRenders, ) is Failure -> oldState.copy( @@ -216,15 +225,15 @@ sealed interface SubPostsPartialChange : PartialChange { ) } - object Start : Load() + data object Start : Load() + data class Success( val anti: ImmutableHolder, val forum: ImmutableHolder, val thread: ImmutableHolder, val post: ImmutableHolder, val postContentRenders: ImmutableList, - val subPosts: ImmutableList>, - val subPostsContentRenders: ImmutableList>, + val subPosts: ImmutableList, val hasMore: Boolean, val currentPage: Int, val totalPage: Int, @@ -248,7 +257,6 @@ sealed interface SubPostsPartialChange : PartialChange { totalPage = totalPage, totalCount = totalCount, subPosts = (oldState.subPosts + subPosts).toImmutableList(), - subPostsContentRenders = (oldState.subPostsContentRenders + subPostsContentRenders).toImmutableList(), ) is Failure -> oldState.copy( @@ -256,10 +264,10 @@ sealed interface SubPostsPartialChange : PartialChange { ) } - object Start : LoadMore() + data object Start : LoadMore() + data class Success( - val subPosts: ImmutableList>, - val subPostsContentRenders: ImmutableList>, + val subPosts: ImmutableList, val hasMore: Boolean, val currentPage: Int, val totalPage: Int, @@ -270,13 +278,13 @@ sealed interface SubPostsPartialChange : PartialChange { } sealed class Agree : SubPostsPartialChange { - private fun List>.updateAgreeStatus( + private fun List.updateAgreeStatus( subPostId: Long, - hasAgreed: Boolean - ): ImmutableList> = + hasAgreed: Boolean, + ): ImmutableList = map { - if (it.get { id } == subPostId) { - it.getImmutable { updateAgreeStatus(if (hasAgreed) 1 else 0) } + if (it.id == subPostId) { + it.updateAgreeStatus(if (hasAgreed) 1 else 0) } else { it } @@ -341,13 +349,9 @@ sealed interface SubPostsPartialChange : PartialChange { 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 - ), + subPosts = oldState.subPosts.filter { it.id != subPostId } + .toImmutableList(), ) } } @@ -362,11 +366,35 @@ sealed interface SubPostsPartialChange : PartialChange { data class Failure( val errorCode: Int, - val errorMessage: String + val errorMessage: String, ) : DeletePost() } } +@Immutable +data class SubPostItemData( + val subPost: ImmutableHolder, + val subPostContentRenders: ImmutableList, + val blocked: Boolean = subPost.get { shouldBlock() }, +) { + constructor( + subPost: SubPostList, + ) : this( + subPost.wrapImmutable(), + subPost.content.renders.toImmutableList(), + subPost.shouldBlock() + ) + + val id: Long + get() = subPost.get { id } + + val author: ImmutableHolder? + get() = subPost.get { author }?.wrapImmutable() +} + +private fun SubPostItemData.updateAgreeStatus(hasAgreed: Int): SubPostItemData = + copy(subPost = subPost.getImmutable { updateAgreeStatus(hasAgreed) }) + data class SubPostsUiState( val isLoading: Boolean = false, val isRefreshing: Boolean = false, @@ -381,10 +409,9 @@ data class SubPostsUiState( val thread: ImmutableHolder? = null, val post: ImmutableHolder? = null, val postContentRenders: ImmutableList = persistentListOf(), - val subPosts: ImmutableList> = persistentListOf(), - val subPostsContentRenders: ImmutableList> = persistentListOf(), + val subPosts: ImmutableList = persistentListOf(), ) : UiState sealed interface SubPostsUiEvent : UiEvent { - object ScrollToSubPosts : SubPostsUiEvent + data object ScrollToSubPosts : SubPostsUiEvent } \ No newline at end of file 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 57def8d9..aeca2a78 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 @@ -85,7 +85,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.util.fastForEach -import androidx.compose.ui.util.fastForEachIndexed import com.huanchengfly.tieba.post.App import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.activities.UserActivity @@ -125,6 +124,8 @@ import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination import com.huanchengfly.tieba.post.ui.widgets.Chip import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar import com.huanchengfly.tieba.post.ui.widgets.compose.BackNavigationIcon +import com.huanchengfly.tieba.post.ui.widgets.compose.BlockTip +import com.huanchengfly.tieba.post.ui.widgets.compose.BlockableContent import com.huanchengfly.tieba.post.ui.widgets.compose.Button import com.huanchengfly.tieba.post.ui.widgets.compose.Card import com.huanchengfly.tieba.post.ui.widgets.compose.ConfirmDialog @@ -799,13 +800,13 @@ fun ThreadPage( fun PostCard( item: ImmutableHolder, contentRenders: ImmutableList, - subPostContents: ImmutableList, - blocked: Boolean + subPosts: ImmutableList, + blocked: Boolean, ) { PostCard( postHolder = item, contentRenders = contentRenders, - subPostContents = subPostContents, + subPosts = subPosts, threadAuthorId = author?.get { id } ?: 0L, blocked = blocked, canDelete = { it.author_id == user.get { id } }, @@ -920,11 +921,11 @@ fun ThreadPage( items( items = latestPosts, key = { (item) -> "LatestPost_${item.get { id }}" } - ) { (item, blocked, renders, subPostContents) -> + ) { (item, blocked, renders, subPosts) -> PostCard( item, renders, - subPostContents, + subPosts, blocked ) } @@ -1295,11 +1296,11 @@ fun ThreadPage( items( items = data, key = { (item) -> "Post_${item.get { id }}" } - ) { (item, blocked, renders, subPostContents) -> + ) { (item, blocked, renders, subPosts) -> PostCard( item, renders, - subPostContents, + subPosts, blocked ) } @@ -1484,7 +1485,7 @@ private fun BottomBar( fun PostCard( postHolder: ImmutableHolder, contentRenders: ImmutableList, - subPostContents: ImmutableList = persistentListOf(), + subPosts: ImmutableList = persistentListOf(), threadAuthorId: Long = 0L, blocked: Boolean = false, canDelete: (Post) -> Boolean = { false }, @@ -1499,26 +1500,6 @@ fun PostCard( onMenuDeleteClick: ((Post) -> Unit)? = null, ) { val context = LocalContext.current - if (blocked && !immersiveMode) { - val hideBlockedContent = context.appPreferences.hideBlockedContent - if (!hideBlockedContent) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 16.dp) - .clip(RoundedCornerShape(6.dp)) - .background(ExtendedTheme.colors.floorCard) - .padding(vertical = 8.dp, horizontal = 16.dp) - ) { - Text( - text = stringResource(id = R.string.tip_blocked_post, postHolder.get { floor }), - style = MaterialTheme.typography.caption, - color = ExtendedTheme.colors.textSecondary - ) - } - } - return - } val post = remember(postHolder) { postHolder.get() } val hasPadding = remember(key1 = postHolder, key2 = immersiveMode) { postHolder.get { floor > 1 } && !immersiveMode @@ -1534,188 +1515,214 @@ fun PostCard( val agreeNum = remember(postHolder) { post.agree?.diffAgreeNum ?: 0L } - val subPosts = remember(postHolder) { - postHolder.get { sub_post_list?.sub_post_list }?.wrapImmutable() ?: persistentListOf() - } val menuState = rememberMenuState() - LongClickMenu( - menuState = menuState, - indication = null, - onClick = { - onReplyClick(post) + BlockableContent( + blocked = blocked, + blockedTip = { + BlockTip { + Text( + text = stringResource(id = R.string.tip_blocked_post, postHolder.get { floor }), + ) + } }, - menuContent = { - DropdownMenuItem( - onClick = { - onReplyClick(post) - menuState.expanded = false - } - ) { - Text(text = stringResource(id = R.string.btn_reply)) - } - 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( - onClick = { - onMenuFavoriteClick(post) - menuState.expanded = false - } - ) { - if (isCollected(post)) { - Text(text = stringResource(id = R.string.title_collect_on)) - } else { - Text(text = stringResource(id = R.string.title_collect_floor)) - } - } - } - if (canDelete(post) && onMenuDeleteClick != null) { - DropdownMenuItem( - onClick = { - onMenuDeleteClick(post) - menuState.expanded = false - } - ) { - Text(text = stringResource(id = R.string.title_delete)) - } - } - } + hideBlockedContent = context.appPreferences.hideBlockedContent || immersiveMode, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp) ) { - Card( - header = { - if (!immersiveMode) { - UserHeader( - avatar = { - Avatar( - data = StringUtil.getAvatarUrl(author.portrait), - size = Sizes.Small, - contentDescription = null - ) - }, - name = { - UserNameText( - userName = StringUtil.getUsernameAnnotatedString( - LocalContext.current, - author.name, - author.nameShow - ), - userLevel = author.level_id, - isLz = author.id == threadAuthorId, - bawuType = author.bawuType, - ) - }, - desc = { - Text( - text = getDescText( - post.time.toLong(), - post.floor, - author.ip_address - ) - ) - }, + LongClickMenu( + menuState = menuState, + indication = null, + onClick = { + onReplyClick(post) + }, + menuContent = { + DropdownMenuItem( + onClick = { + onReplyClick(post) + menuState.expanded = false + } + ) { + Text(text = stringResource(id = R.string.btn_reply)) + } + 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( onClick = { - UserActivity.launch(context, author.id.toString()) + onMenuFavoriteClick(post) + menuState.expanded = false } ) { - if (post.floor > 1) { - PostAgreeBtn( - hasAgreed = hasAgreed, - agreeNum = agreeNum, - onClick = onAgree - ) + if (isCollected(post)) { + Text(text = stringResource(id = R.string.title_collect_on)) + } else { + Text(text = stringResource(id = R.string.title_collect_floor)) } } } - }, - content = { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = paddingModifier - .fillMaxWidth() - ) { - if (showTitle) { - Text( - text = post.title, - style = MaterialTheme.typography.subtitle1, - fontSize = 15.sp - ) + if (canDelete(post) && onMenuDeleteClick != null) { + DropdownMenuItem( + onClick = { + onMenuDeleteClick(post) + menuState.expanded = false + } + ) { + Text(text = stringResource(id = R.string.title_delete)) } - - if (isCollected(post)) { - Chip( - text = stringResource(id = R.string.title_collected_floor), - invertColor = true, - icon = { - Icon( - imageVector = Icons.Rounded.Star, - contentDescription = null, - modifier = Modifier.size(16.dp) + } + } + ) { + Card( + header = { + if (!immersiveMode) { + UserHeader( + avatar = { + Avatar( + data = StringUtil.getAvatarUrl(author.portrait), + size = Sizes.Small, + contentDescription = null + ) + }, + name = { + UserNameText( + userName = StringUtil.getUsernameAnnotatedString( + LocalContext.current, + author.name, + author.nameShow + ), + userLevel = author.level_id, + isLz = author.id == threadAuthorId, + bawuType = author.bawuType, + ) + }, + desc = { + Text( + text = getDescText( + post.time.toLong(), + post.floor, + author.ip_address + ) + ) + }, + onClick = { + UserActivity.launch(context, author.id.toString()) + } + ) { + if (post.floor > 1) { + PostAgreeBtn( + hasAgreed = hasAgreed, + agreeNum = agreeNum, + onClick = onAgree ) } - ) + } } - - contentRenders.fastForEach { it.Render() } - } - - if (showSubPosts && post.sub_post_number > 0 && subPostContents.isNotEmpty() && !immersiveMode) { + }, + content = { Column( - modifier = Modifier + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = paddingModifier .fillMaxWidth() - .then(paddingModifier) - .clip(RoundedCornerShape(6.dp)) - .background(ExtendedTheme.colors.floorCard) - .padding(vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(2.dp) ) { - subPostContents.fastForEachIndexed { index, text -> - SubPostItem( - subPostList = subPosts[index], - subPostContent = text, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp), - onReplyClick = { onSubPostReplyClick?.invoke(post, it) }, - onOpenSubPosts = onOpenSubPosts, + if (showTitle) { + Text( + text = post.title, + style = MaterialTheme.typography.subtitle1, + fontSize = 15.sp ) } - if (post.sub_post_number > subPostContents.size) { - Text( - text = stringResource( - id = R.string.open_all_sub_posts, - post.sub_post_number - ), - style = MaterialTheme.typography.caption, - fontSize = 13.sp, - color = ExtendedTheme.colors.accent, - modifier = Modifier - .fillMaxWidth() - .padding(top = 2.dp) - .clickable { - onOpenSubPosts(0) - } - .padding(horizontal = 12.dp) + if (isCollected(post)) { + Chip( + text = stringResource(id = R.string.title_collected_floor), + invertColor = true, + icon = { + Icon( + imageVector = Icons.Rounded.Star, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + } ) } + + contentRenders.fastForEach { it.Render() } + } + + if (showSubPosts && post.sub_post_number > 0 && subPosts.isNotEmpty() && !immersiveMode) { + Column( + modifier = Modifier + .fillMaxWidth() + .then(paddingModifier) + .clip(RoundedCornerShape(6.dp)) + .background(ExtendedTheme.colors.floorCard) + .padding(vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + subPosts.fastForEach { item -> + BlockableContent( + blocked = item.blocked, + blockedTip = { + Text( + text = stringResource(id = R.string.tip_blocked_sub_post), + style = MaterialTheme.typography.body2.copy( + color = ExtendedTheme.colors.textDisabled, + fontSize = 13.sp + ), + modifier = Modifier.padding(horizontal = 12.dp) + ) + }, + ) { + SubPostItem( + subPostList = item.subPost, + subPostContent = item.subPostContent, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp), + onReplyClick = { onSubPostReplyClick?.invoke(post, it) }, + onOpenSubPosts = onOpenSubPosts, + ) + } + } + + if (post.sub_post_number > subPosts.size) { + Text( + text = stringResource( + id = R.string.open_all_sub_posts, + post.sub_post_number + ), + style = MaterialTheme.typography.caption, + fontSize = 13.sp, + color = ExtendedTheme.colors.accent, + modifier = Modifier + .fillMaxWidth() + .padding(top = 2.dp) + .clickable { + onOpenSubPosts(0) + } + .padding(horizontal = 12.dp) + ) + } + } } } - } - ) + ) + } } } 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 e0916d60..4f50568b 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 @@ -11,12 +11,14 @@ 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 import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo import com.huanchengfly.tieba.post.api.models.protos.User import com.huanchengfly.tieba.post.api.models.protos.contentRenders import com.huanchengfly.tieba.post.api.models.protos.pbPage.PbPageResponse import com.huanchengfly.tieba.post.api.models.protos.renders import com.huanchengfly.tieba.post.api.models.protos.subPostContents +import com.huanchengfly.tieba.post.api.models.protos.subPosts import com.huanchengfly.tieba.post.api.models.protos.updateAgreeStatus import com.huanchengfly.tieba.post.api.models.protos.updateCollectStatus import com.huanchengfly.tieba.post.api.retrofit.exception.TiebaUnknownException @@ -1137,5 +1139,18 @@ data class PostItemData( val post: ImmutableHolder, val blocked: Boolean = post.get { shouldBlock() }, val contentRenders: ImmutableList = post.get { this.contentRenders }, - val subPostContents: ImmutableList = post.get { this.subPostContents }, -) \ No newline at end of file + val subPosts: ImmutableList = post.get { this.subPosts }, +) + +@Immutable +data class SubPostItemData( + val subPost: ImmutableHolder, + val subPostContent: AnnotatedString, + val blocked: Boolean = subPost.get { shouldBlock() }, +) { + val id: Long + get() = subPost.get { id } + + val author: ImmutableHolder? + get() = subPost.get { author }?.wrapImmutable() +} \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Block.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Block.kt new file mode 100644 index 00000000..4c045c76 --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Block.kt @@ -0,0 +1,55 @@ +package com.huanchengfly.tieba.post.ui.widgets.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.huanchengfly.tieba.post.R +import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme +import com.huanchengfly.tieba.post.utils.appPreferences + +@Composable +fun BlockTip( + text: @Composable () -> Unit = { Text(text = stringResource(id = R.string.tip_blocked_content)) }, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(6.dp)) + .background(ExtendedTheme.colors.textSecondary.copy(alpha = 0.1f)) + .padding(vertical = 8.dp, horizontal = 16.dp) + ) { + ProvideTextStyle(value = MaterialTheme.typography.caption) { + ProvideContentColor(color = ExtendedTheme.colors.textSecondary) { + text() + } + } + } +} + +@Composable +fun BlockableContent( + blocked: Boolean, + modifier: Modifier = Modifier, + blockedTip: @Composable () -> Unit = { BlockTip() }, + hideBlockedContent: Boolean = LocalContext.current.appPreferences.hideBlockedContent, + content: @Composable () -> Unit, +) { + if (!blocked) { + content() + } else if (!hideBlockedContent) { + Column(modifier = modifier) { + blockedTip() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/utils/BlockManager.kt b/app/src/main/java/com/huanchengfly/tieba/post/utils/BlockManager.kt index 18c49c50..70b35c99 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/utils/BlockManager.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/utils/BlockManager.kt @@ -1,6 +1,7 @@ package com.huanchengfly.tieba.post.utils import com.huanchengfly.tieba.post.api.models.protos.Post +import com.huanchengfly.tieba.post.api.models.protos.SubPostList import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo import com.huanchengfly.tieba.post.api.models.protos.abstractText import com.huanchengfly.tieba.post.api.models.protos.plainText @@ -60,4 +61,7 @@ object BlockManager { fun Post.shouldBlock(): Boolean = shouldBlock(content.plainText) || shouldBlock(author_id, author?.name) + + fun SubPostList.shouldBlock(): Boolean = + shouldBlock(content.plainText) || shouldBlock(author_id, author?.name) } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 121f7fb1..aff1415c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -675,6 +675,7 @@ 查看全部 %d 条回复 贴子更新到了第 %d 楼 由于你的屏蔽设置,第 %d 楼已被屏蔽 + 由于你的屏蔽设置,该回复已被屏蔽 不看视频贴 全屏 播放 @@ -707,6 +708,7 @@ 以下为最新回复 查看原贴 由于你的屏蔽设置,该贴已被屏蔽 + 由于你的屏蔽设置,该内容已被屏蔽 图片 该贴已被删除 展开