feat: 热榜页面贴子卡片 & 点赞
This commit is contained in:
parent
dff92beadc
commit
a5c32056c0
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ThreadInfo>,
|
||||
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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<HotUiIntent.RefreshThreadList>()
|
||||
.flatMapConcat { it.producePartialChange() },
|
||||
intentFlow.filterIsInstance<HotUiIntent.Agree>()
|
||||
.flatMapConcat { it.producePartialChange() },
|
||||
)
|
||||
|
||||
private fun produceLoadPartialChange(): Flow<HotPartialChange.Load> =
|
||||
|
|
@ -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<HotPartialChange.Agree> =
|
||||
TiebaApi.getInstance()
|
||||
.opAgreeFlow(
|
||||
threadId.toString(), postId.toString(), hasAgree, objType = 3
|
||||
)
|
||||
.map<AgreeBean, HotPartialChange.Agree> {
|
||||
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<HotUiState> {
|
||||
|
|
@ -81,9 +108,10 @@ sealed interface HotPartialChange : PartialChange<HotUiState> {
|
|||
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<HotUiState> {
|
|||
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<HotUiState> {
|
|||
val error: Throwable
|
||||
) : RefreshThreadList()
|
||||
}
|
||||
|
||||
sealed class Agree private constructor() : HotPartialChange {
|
||||
private fun List<ImmutableHolder<ThreadInfo>>.updateAgreeStatus(
|
||||
threadId: Long,
|
||||
hasAgree: Int
|
||||
): ImmutableList<ImmutableHolder<ThreadInfo>> {
|
||||
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<RecommendTopicList> = emptyList(),
|
||||
val tabList: List<FrsTabInfo> = emptyList(),
|
||||
val threadList: List<ThreadInfo> = emptyList(),
|
||||
val topicList: ImmutableList<ImmutableHolder<RecommendTopicList>> = persistentListOf(),
|
||||
val tabList: ImmutableList<ImmutableHolder<FrsTabInfo>> = persistentListOf(),
|
||||
val threadList: ImmutableList<ImmutableHolder<ThreadInfo>> = persistentListOf(),
|
||||
) : UiState
|
||||
|
||||
sealed interface HotUiEvent : UiEvent
|
||||
|
|
@ -114,14 +114,16 @@ class PersonalizedViewModel @Inject constructor() :
|
|||
.onStart { emit(PersonalizedPartialChange.Dislike.Start(threadId)) }
|
||||
|
||||
private fun PersonalizedUiIntent.Agree.producePartialChange(): Flow<PersonalizedPartialChange.Agree> =
|
||||
TiebaApi.getInstance().opAgreeFlow(
|
||||
threadId.toString(), postId.toString(), hasAgree, objType = 3
|
||||
).map<AgreeBean, PersonalizedPartialChange.Agree> {
|
||||
PersonalizedPartialChange.Agree.Success(
|
||||
threadId,
|
||||
hasAgree xor 1
|
||||
TiebaApi.getInstance()
|
||||
.opAgreeFlow(
|
||||
threadId.toString(), postId.toString(), hasAgree, objType = 3
|
||||
)
|
||||
}
|
||||
.map<AgreeBean, PersonalizedPartialChange.Agree> {
|
||||
PersonalizedPartialChange.Agree.Success(
|
||||
threadId,
|
||||
hasAgree xor 1
|
||||
)
|
||||
}
|
||||
.catch { emit(PersonalizedPartialChange.Agree.Failure(threadId, hasAgree, it)) }
|
||||
.onStart { emit(PersonalizedPartialChange.Agree.Start(threadId, hasAgree xor 1)) }
|
||||
|
||||
|
|
|
|||
|
|
@ -485,7 +485,7 @@ private fun ThreadShareBtn(
|
|||
fun FeedCard(
|
||||
item: ImmutableHolder<ThreadInfo>,
|
||||
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)
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue