diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/Utils.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/Utils.kt index 57a088e9..9ce0bfe3 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/api/Utils.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/Utils.kt @@ -57,6 +57,12 @@ val ThreadInfo.abstractText: String } } +val ThreadInfo.hasAgree: Int + get() = agree?.hasAgree ?: 0 + +val ThreadInfo.hasAgreed: Boolean + get() = hasAgree == 1 + val ThreadInfo.hasAbstract: Boolean get() = richAbstract.any { (it.type == 0 && it.text.isNotBlank()) || it.type == 2 } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotPage.kt index ac329999..a13fa717 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotPage.kt @@ -31,6 +31,7 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -45,7 +46,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.google.accompanist.placeholder.material.placeholder import com.huanchengfly.tieba.post.R +import com.huanchengfly.tieba.post.api.hasAgree import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo +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 @@ -56,7 +59,9 @@ import com.huanchengfly.tieba.post.ui.common.theme.compose.White import com.huanchengfly.tieba.post.ui.common.theme.compose.Yellow import com.huanchengfly.tieba.post.ui.page.LocalNavigator import com.huanchengfly.tieba.post.ui.page.destinations.HotTopicListPageDestination +import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent +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.NetworkImage import com.huanchengfly.tieba.post.ui.widgets.compose.ProvideContentColor @@ -155,12 +160,12 @@ fun HotPage( modifier = Modifier.padding(bottom = 2.dp) ) Text( - text = item.topicName, + text = item.get { topicName }, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f) ) - when (item.tag) { + when (item.get { tag }) { 2 -> Text( text = stringResource(id = R.string.topic_tag_hot), fontSize = 10.sp, @@ -220,7 +225,7 @@ fun HotPage( } if (threadList.isNotEmpty()) { if (tabList.isNotEmpty()) { - stickyHeader(key = "ThreadTabs") { + item(key = "ThreadTabs") { VerticalGrid( column = 5, modifier = Modifier @@ -239,9 +244,9 @@ fun HotPage( } items(tabList) { ThreadListTab( - text = it.tabName, - selected = currentTabCode == it.tabCode, - onSelected = { viewModel.send(HotUiIntent.RefreshThreadList(it.tabCode)) } + text = it.get { tabName }, + selected = currentTabCode == it.get { tabCode }, + onSelected = { viewModel.send(HotUiIntent.RefreshThreadList(it.get { tabCode })) } ) } } @@ -266,8 +271,68 @@ fun HotPage( } else { itemsIndexed( items = threadList, - key = { _, item -> "Thread_${item.threadId}" }) { index, item -> - ThreadListItem(index = index, item = item) + key = { _, item -> "Thread_${item.get { threadId }}" } + ) { index, item -> + FeedCard( + item = item, + onClick = { + navigator.navigate( + ThreadPageDestination( + threadId = it.id, + threadInfo = it + ) + ) + }, + onAgree = { + viewModel.send( + HotUiIntent.Agree( + threadId = it.threadId, + postId = it.firstPostId, + hasAgree = it.hasAgree + ) + ) + }, + ) { + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + val color = when (index) { + 0 -> RedA700 + 1 -> OrangeA700 + 2 -> Yellow + else -> MaterialTheme.colors.onBackground.copy( + ContentAlpha.medium + ) + } + Text( + text = "${index + 1}", + color = color, + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + Text( + text = stringResource( + id = R.string.hot_num, + item.get { hotNum }.getShortNumString() + ), + style = MaterialTheme.typography.caption, + color = color + ) + } + } +// ThreadListItem( +// index = index, +// itemHolder = item, +// onClick = { +// navigator.navigate( +// ThreadPageDestination( +// threadId = it.id, +// threadInfo = it +// ) +// ) +// } +// ) } } } @@ -322,13 +387,14 @@ private fun ThreadListItemPlaceholder() { @Composable private fun ThreadListItem( index: Int, - item: ThreadInfo, - onClick: () -> Unit = {} + itemHolder: ImmutableHolder, + onClick: (ThreadInfo) -> Unit = {} ) { + val item = remember(itemHolder) { itemHolder.get() } val heightModifier = if (item.media.isEmpty()) Modifier else Modifier.height(80.dp) Row( modifier = Modifier - .clickable(onClick = onClick) + .clickable { onClick(item) } .padding(all = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { @@ -368,7 +434,14 @@ private fun ThreadListItem( ) } } - if (item.media.isNotEmpty()) { + if (!item.videoInfo?.thumbnailUrl.isNullOrBlank()) { + NetworkImage( + imageUri = item.videoInfo?.thumbnailUrl!!, + contentDescription = null, + modifier = heightModifier.aspectRatio(16f / 9), + contentScale = ContentScale.Crop + ) + } else if (!item.media.firstOrNull()?.dynamicPic.isNullOrBlank()) { NetworkImage( imageUri = item.media.first().dynamicPic, contentDescription = null, @@ -397,7 +470,7 @@ private fun ThreadListTab( .clip(RoundedCornerShape(100)) .background(backgroundColor) .clickable(onClick = onSelected) - .padding(horizontal = 16.dp, vertical = 4.dp), + .padding(vertical = 4.dp), fontSize = 12.sp, fontWeight = FontWeight.Bold ) diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotViewModel.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotViewModel.kt index 58f9a105..9b00980f 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotViewModel.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotViewModel.kt @@ -2,17 +2,22 @@ package com.huanchengfly.tieba.post.ui.page.main.explore.hot 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.protos.FrsTabInfo import com.huanchengfly.tieba.post.api.models.protos.RecommendTopicList import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo import com.huanchengfly.tieba.post.api.models.protos.hotThreadList.HotThreadListResponse import com.huanchengfly.tieba.post.arch.BaseViewModel +import com.huanchengfly.tieba.post.arch.ImmutableHolder import com.huanchengfly.tieba.post.arch.PartialChange import com.huanchengfly.tieba.post.arch.PartialChangeProducer 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 dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch @@ -41,6 +46,8 @@ class HotViewModel @Inject constructor() : .flatMapConcat { produceLoadPartialChange() }, intentFlow.filterIsInstance() .flatMapConcat { it.producePartialChange() }, + intentFlow.filterIsInstance() + .flatMapConcat { it.producePartialChange() }, ) private fun produceLoadPartialChange(): Flow = @@ -65,6 +72,20 @@ class HotViewModel @Inject constructor() : } .onStart { emit(HotPartialChange.RefreshThreadList.Start(tabCode)) } .catch { emit(HotPartialChange.RefreshThreadList.Failure(tabCode, it)) } + + private fun HotUiIntent.Agree.producePartialChange(): Flow = + TiebaApi.getInstance() + .opAgreeFlow( + threadId.toString(), postId.toString(), hasAgree, objType = 3 + ) + .map { + HotPartialChange.Agree.Success( + threadId, + hasAgree xor 1 + ) + } + .onStart { emit(HotPartialChange.Agree.Start(threadId, hasAgree xor 1)) } + .catch { emit(HotPartialChange.Agree.Failure(threadId, hasAgree, it)) } } } @@ -72,6 +93,12 @@ sealed interface HotUiIntent : UiIntent { object Load : HotUiIntent data class RefreshThreadList(val tabCode: String) : HotUiIntent + + data class Agree( + val threadId: Long, + val postId: Long, + val hasAgree: Int + ) : HotUiIntent } sealed interface HotPartialChange : PartialChange { @@ -81,9 +108,10 @@ sealed interface HotPartialChange : PartialChange { Start -> oldState.copy(isRefreshing = true) is Success -> oldState.copy( isRefreshing = false, - topicList = topicList, - tabList = tabList, - threadList = threadList + currentTabCode = "all", + topicList = topicList.wrapImmutable(), + tabList = tabList.wrapImmutable(), + threadList = threadList.wrapImmutable() ) is Failure -> oldState.copy(isRefreshing = false) @@ -109,7 +137,7 @@ sealed interface HotPartialChange : PartialChange { is Success -> oldState.copy( isLoadingThreadList = false, currentTabCode = tabCode, - threadList = threadList + threadList = threadList.wrapImmutable() ) is Failure -> oldState.copy(isLoadingThreadList = false) @@ -127,15 +155,105 @@ sealed interface HotPartialChange : PartialChange { val error: Throwable ) : RefreshThreadList() } + + sealed class Agree private constructor() : HotPartialChange { + private fun List>.updateAgreeStatus( + threadId: Long, + hasAgree: Int + ): ImmutableList> { + return map { + val threadInfo = it.get() + if (threadInfo.threadId == threadId) { + if (threadInfo.agree != null) { + if (hasAgree != threadInfo.agree.hasAgree) { + if (hasAgree == 1) { + threadInfo.copy( + agreeNum = threadInfo.agreeNum + 1, + agree = threadInfo.agree.copy( + agreeNum = threadInfo.agree.agreeNum + 1, + diffAgreeNum = threadInfo.agree.diffAgreeNum + 1, + hasAgree = 1 + ) + ) + } else { + threadInfo.copy( + agreeNum = threadInfo.agreeNum - 1, + agree = threadInfo.agree.copy( + agreeNum = threadInfo.agree.agreeNum - 1, + diffAgreeNum = threadInfo.agree.diffAgreeNum - 1, + hasAgree = 0 + ) + ) + } + } else { + threadInfo + } + } else { + threadInfo.copy( + agreeNum = if (hasAgree == 1) threadInfo.agreeNum + 1 else threadInfo.agreeNum - 1 + ) + } + } else { + threadInfo + } + }.wrapImmutable() + } + + override fun reduce(oldState: HotUiState): HotUiState = + when (this) { + is Start -> { + oldState.copy( + threadList = oldState.threadList.updateAgreeStatus( + threadId, + hasAgree + ) + ) + } + + is Success -> { + oldState.copy( + threadList = oldState.threadList.updateAgreeStatus( + threadId, + hasAgree + ) + ) + } + + is Failure -> { + oldState.copy( + threadList = oldState.threadList.updateAgreeStatus( + threadId, + hasAgree + ) + ) + } + } + + data class Start( + val threadId: Long, + val hasAgree: Int + ) : Agree() + + data class Success( + val threadId: Long, + val hasAgree: Int + ) : Agree() + + data class Failure( + val threadId: Long, + val hasAgree: Int, + val error: Throwable + ) : Agree() + } } data class HotUiState( val isRefreshing: Boolean = true, val currentTabCode: String = "all", val isLoadingThreadList: Boolean = false, - val topicList: List = emptyList(), - val tabList: List = emptyList(), - val threadList: List = emptyList(), + val topicList: ImmutableList> = persistentListOf(), + val tabList: ImmutableList> = persistentListOf(), + val threadList: ImmutableList> = persistentListOf(), ) : UiState sealed interface HotUiEvent : UiEvent \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedViewModel.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedViewModel.kt index 12c58ad9..fde024b7 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedViewModel.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedViewModel.kt @@ -114,14 +114,16 @@ class PersonalizedViewModel @Inject constructor() : .onStart { emit(PersonalizedPartialChange.Dislike.Start(threadId)) } private fun PersonalizedUiIntent.Agree.producePartialChange(): Flow = - TiebaApi.getInstance().opAgreeFlow( - threadId.toString(), postId.toString(), hasAgree, objType = 3 - ).map { - PersonalizedPartialChange.Agree.Success( - threadId, - hasAgree xor 1 + TiebaApi.getInstance() + .opAgreeFlow( + threadId.toString(), postId.toString(), hasAgree, objType = 3 ) - } + .map { + PersonalizedPartialChange.Agree.Success( + threadId, + hasAgree xor 1 + ) + } .catch { emit(PersonalizedPartialChange.Agree.Failure(threadId, hasAgree, it)) } .onStart { emit(PersonalizedPartialChange.Agree.Start(threadId, hasAgree xor 1)) } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt index 6da81c37..ce752994 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt @@ -485,7 +485,7 @@ private fun ThreadShareBtn( fun FeedCard( item: ImmutableHolder, onClick: (ThreadInfo) -> Unit, - onAgree: () -> Unit, + onAgree: (ThreadInfo) -> Unit, onClickForum: () -> Unit = {}, dislikeAction: @Composable () -> Unit = {}, ) { @@ -526,7 +526,7 @@ fun FeedCard( ThreadAgreeBtn( hasAgree = item.get { agree?.hasAgree == 1 }, agreeNum = item.get { agreeNum }, - onClick = onAgree, + onClick = { onAgree(item.get()) }, modifier = Modifier.weight(1f) )