feat: 楼中楼屏蔽

This commit is contained in:
HuanCheng65 2023-09-23 17:17:33 +08:00
parent 5272f0f328
commit 478f275ceb
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
10 changed files with 499 additions and 393 deletions

View File

@ -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<AnnotatedString>
?.toImmutableList()
?: persistentListOf()
val Post.subPosts: ImmutableList<SubPostItemData>
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

View File

@ -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,
)
}
}
}

View File

@ -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) {

View File

@ -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<SubPostsUiEvent.ScrollToSubPosts> {
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<SubPostList>,
contentRenders: ImmutableList<PbContentRender>,
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<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 })
)
},
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() }
}
}
}
)
)
}
}
}

View File

@ -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<PbFloorResponse, SubPostsPartialChange.LoadMore> { 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<SubPostsUiState> {
post = post,
postContentRenders = postContentRenders,
subPosts = subPosts,
subPostsContentRenders = subPostsContentRenders,
)
is Failure -> oldState.copy(
@ -216,15 +225,15 @@ sealed interface SubPostsPartialChange : PartialChange<SubPostsUiState> {
)
}
object Start : Load()
data object Start : Load()
data class Success(
val anti: ImmutableHolder<Anti>,
val forum: ImmutableHolder<SimpleForum>,
val thread: ImmutableHolder<ThreadInfo>,
val post: ImmutableHolder<Post>,
val postContentRenders: ImmutableList<PbContentRender>,
val subPosts: ImmutableList<ImmutableHolder<SubPostList>>,
val subPostsContentRenders: ImmutableList<ImmutableList<PbContentRender>>,
val subPosts: ImmutableList<SubPostItemData>,
val hasMore: Boolean,
val currentPage: Int,
val totalPage: Int,
@ -248,7 +257,6 @@ sealed interface SubPostsPartialChange : PartialChange<SubPostsUiState> {
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<SubPostsUiState> {
)
}
object Start : LoadMore()
data object Start : LoadMore()
data class Success(
val subPosts: ImmutableList<ImmutableHolder<SubPostList>>,
val subPostsContentRenders: ImmutableList<ImmutableList<PbContentRender>>,
val subPosts: ImmutableList<SubPostItemData>,
val hasMore: Boolean,
val currentPage: Int,
val totalPage: Int,
@ -270,13 +278,13 @@ sealed interface SubPostsPartialChange : PartialChange<SubPostsUiState> {
}
sealed class Agree : SubPostsPartialChange {
private fun List<ImmutableHolder<SubPostList>>.updateAgreeStatus(
private fun List<SubPostItemData>.updateAgreeStatus(
subPostId: Long,
hasAgreed: Boolean
): ImmutableList<ImmutableHolder<SubPostList>> =
hasAgreed: Boolean,
): ImmutableList<SubPostItemData> =
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<SubPostsUiState> {
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<SubPostsUiState> {
data class Failure(
val errorCode: Int,
val errorMessage: String
val errorMessage: String,
) : DeletePost()
}
}
@Immutable
data class SubPostItemData(
val subPost: ImmutableHolder<SubPostList>,
val subPostContentRenders: ImmutableList<PbContentRender>,
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<User>?
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<ThreadInfo>? = null,
val post: ImmutableHolder<Post>? = null,
val postContentRenders: ImmutableList<PbContentRender> = persistentListOf(),
val subPosts: ImmutableList<ImmutableHolder<SubPostList>> = persistentListOf(),
val subPostsContentRenders: ImmutableList<ImmutableList<PbContentRender>> = persistentListOf(),
val subPosts: ImmutableList<SubPostItemData> = persistentListOf(),
) : UiState
sealed interface SubPostsUiEvent : UiEvent {
object ScrollToSubPosts : SubPostsUiEvent
data object ScrollToSubPosts : SubPostsUiEvent
}

View File

@ -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<Post>,
contentRenders: ImmutableList<PbContentRender>,
subPostContents: ImmutableList<AnnotatedString>,
blocked: Boolean
subPosts: ImmutableList<SubPostItemData>,
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<Post>,
contentRenders: ImmutableList<PbContentRender>,
subPostContents: ImmutableList<AnnotatedString> = persistentListOf(),
subPosts: ImmutableList<SubPostItemData> = 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)
)
}
}
}
}
}
)
)
}
}
}

View File

@ -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<Post>,
val blocked: Boolean = post.get { shouldBlock() },
val contentRenders: ImmutableList<PbContentRender> = post.get { this.contentRenders },
val subPostContents: ImmutableList<AnnotatedString> = post.get { this.subPostContents },
)
val subPosts: ImmutableList<SubPostItemData> = post.get { this.subPosts },
)
@Immutable
data class SubPostItemData(
val subPost: ImmutableHolder<SubPostList>,
val subPostContent: AnnotatedString,
val blocked: Boolean = subPost.get { shouldBlock() },
) {
val id: Long
get() = subPost.get { id }
val author: ImmutableHolder<User>?
get() = subPost.get { author }?.wrapImmutable()
}

View File

@ -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()
}
}
}

View File

@ -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)
}

View File

@ -675,6 +675,7 @@
<string name="open_all_sub_posts">查看全部 %d 条回复</string>
<string name="message_store_thread_update">贴子更新到了第 %d 楼</string>
<string name="tip_blocked_post">由于你的屏蔽设置,第 %d 楼已被屏蔽</string>
<string name="tip_blocked_sub_post">由于你的屏蔽设置,该回复已被屏蔽</string>
<string name="settings_block_video">不看视频贴</string>
<string name="btn_full_screen">全屏</string>
<string name="btn_play">播放</string>
@ -707,6 +708,7 @@
<string name="below_is_latest_post">以下为最新回复</string>
<string name="btn_open_origin_thread">查看原贴</string>
<string name="tip_blocked_thread">由于你的屏蔽设置,该贴已被屏蔽</string>
<string name="tip_blocked_content">由于你的屏蔽设置,该内容已被屏蔽</string>
<string name="desc_picture">图片</string>
<string name="tip_thread_store_deleted">该贴已被删除</string>
<string name="button_expand">展开</string>