feat: 消息列表屏蔽

This commit is contained in:
HuanCheng65 2023-09-23 17:27:58 +08:00
parent 478f275ceb
commit 6527155925
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
5 changed files with 176 additions and 101 deletions

View File

@ -341,7 +341,6 @@ val Post.subPosts: ImmutableList<SubPostItemData>
?.toImmutableList() ?.toImmutableList()
?: persistentListOf() ?: persistentListOf()
@OptIn(ExperimentalTextApi::class)
fun SubPostList.getContentText(threadAuthorId: Long? = null): AnnotatedString { fun SubPostList.getContentText(threadAuthorId: Long? = null): AnnotatedString {
val context = App.INSTANCE val context = App.INSTANCE
val accentColor = Color(ThemeUtils.getColorByAttr(context, R.attr.colorNewPrimary)) val accentColor = Color(ThemeUtils.getColorByAttr(context, R.attr.colorNewPrimary))

View File

@ -36,6 +36,8 @@ import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.destinations.SubPostsPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.SubPostsPageDestination
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar 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.EmoticonText import com.huanchengfly.tieba.post.ui.widgets.compose.EmoticonText
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad 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.LoadMoreLayout
@ -43,6 +45,7 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes
import com.huanchengfly.tieba.post.ui.widgets.compose.UserHeader import com.huanchengfly.tieba.post.ui.widgets.compose.UserHeader
import com.huanchengfly.tieba.post.utils.DateTimeUtils import com.huanchengfly.tieba.post.utils.DateTimeUtils
import com.huanchengfly.tieba.post.utils.StringUtil import com.huanchengfly.tieba.post.utils.StringUtil
import kotlinx.collections.immutable.persistentListOf
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@ -73,7 +76,7 @@ fun NotificationsListPage(
) )
val data by viewModel.uiState.collectPartialAsState( val data by viewModel.uiState.collectPartialAsState(
prop1 = NotificationsListUiState::data, prop1 = NotificationsListUiState::data,
initial = emptyList() initial = persistentListOf()
) )
val currentPage by viewModel.uiState.collectPartialAsState( val currentPage by viewModel.uiState.collectPartialAsState(
prop1 = NotificationsListUiState::currentPage, prop1 = NotificationsListUiState::currentPage,
@ -99,24 +102,37 @@ fun NotificationsListPage(
) { ) {
items( items(
items = data, items = data,
key = { "${it.postId}_${it.replyer?.id}_${it.time}" }, key = { "${it.info.postId}_${it.info.replyer?.id}_${it.info.time}" },
) {
val (info, blocked) = it
BlockableContent(
blocked = blocked,
blockedTip = {
BlockTip {
Text(
text = stringResource(id = R.string.tip_blocked_message)
)
}
},
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 12.dp)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.clickable { .clickable {
if (it.isFloor == "1") { if (info.isFloor == "1") {
navigator.navigate( navigator.navigate(
SubPostsPageDestination( SubPostsPageDestination(
threadId = it.threadId!!.toLong(), threadId = info.threadId!!.toLong(),
subPostId = it.postId!!.toLong(), subPostId = info.postId!!.toLong(),
loadFromSubPost = true loadFromSubPost = true
) )
) )
} else { } else {
navigator.navigate( navigator.navigate(
ThreadPageDestination( ThreadPageDestination(
threadId = it.threadId!!.toLong(), threadId = info.threadId!!.toLong(),
postId = it.postId!!.toLong() postId = info.postId!!.toLong()
) )
) )
} }
@ -124,46 +140,49 @@ fun NotificationsListPage(
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
if (it.replyer != null) { if (info.replyer != null) {
UserHeader( UserHeader(
avatar = { avatar = {
Avatar( Avatar(
data = StringUtil.getAvatarUrl(it.replyer.portrait), data = StringUtil.getAvatarUrl(info.replyer.portrait),
size = Sizes.Small, size = Sizes.Small,
contentDescription = null contentDescription = null
) )
}, },
name = { name = {
Text( Text(
text = it.replyer.nameShow ?: it.replyer.name ?: "" text = info.replyer.nameShow ?: info.replyer.name ?: ""
) )
}, },
onClick = { onClick = {
UserActivity.launch( UserActivity.launch(
context, context,
it.replyer.id!!, info.replyer.id!!,
StringUtil.getAvatarUrl(it.replyer.portrait) StringUtil.getAvatarUrl(info.replyer.portrait)
) )
}, },
desc = { desc = {
Text( Text(
text = DateTimeUtils.getRelativeTimeString( text = DateTimeUtils.getRelativeTimeString(
LocalContext.current, LocalContext.current,
it.time!! info.time!!
) )
) )
}, },
) {} ) {}
} }
EmoticonText(text = it.content ?: "") EmoticonText(text = info.content ?: "")
val quoteText = if (type == NotificationsType.ReplyMe) { val quoteText = if (type == NotificationsType.ReplyMe) {
if ("1" == it.isFloor) { if ("1" == info.isFloor) {
it.quoteContent info.quoteContent
} else { } else {
stringResource(id = R.string.text_message_list_item_reply_my_thread, it.title ?: "") stringResource(
id = R.string.text_message_list_item_reply_my_thread,
info.title ?: ""
)
} }
} else { } else {
it.title info.title
} }
if (quoteText != null) { if (quoteText != null) {
EmoticonText( EmoticonText(
@ -172,23 +191,26 @@ fun NotificationsListPage(
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(6.dp)) .clip(RoundedCornerShape(6.dp))
.clickable { .clickable {
if ("1" == it.isFloor && it.quotePid != null) { if ("1" == info.isFloor && info.quotePid != null) {
navigator.navigate( navigator.navigate(
SubPostsPageDestination( SubPostsPageDestination(
threadId = it.threadId!!.toLong(), threadId = info.threadId!!.toLong(),
postId = it.quotePid.toLong(), postId = info.quotePid.toLong(),
loadFromSubPost = true, loadFromSubPost = true,
) )
) )
} else { } else {
navigator.navigate( navigator.navigate(
ThreadPageDestination( ThreadPageDestination(
threadId = it.threadId!!.toLong(), threadId = info.threadId!!.toLong(),
) )
) )
} }
} }
.background(ExtendedTheme.colors.chip, RoundedCornerShape(6.dp)) .background(
ExtendedTheme.colors.chip,
RoundedCornerShape(6.dp)
)
.padding(8.dp), .padding(8.dp),
color = ExtendedTheme.colors.onChip, color = ExtendedTheme.colors.onChip,
fontSize = 12.sp, fontSize = 12.sp,
@ -198,6 +220,7 @@ fun NotificationsListPage(
} }
} }
} }
}
PullRefreshIndicator( PullRefreshIndicator(
refreshing = isRefreshing, refreshing = isRefreshing,

View File

@ -1,13 +1,31 @@
package com.huanchengfly.tieba.post.ui.page.main.notifications.list package com.huanchengfly.tieba.post.ui.page.main.notifications.list
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.util.fastMap
import com.huanchengfly.tieba.post.api.TiebaApi import com.huanchengfly.tieba.post.api.TiebaApi
import com.huanchengfly.tieba.post.api.models.MessageListBean import com.huanchengfly.tieba.post.api.models.MessageListBean
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage
import com.huanchengfly.tieba.post.arch.* import com.huanchengfly.tieba.post.arch.BaseViewModel
import com.huanchengfly.tieba.post.arch.CommonUiEvent
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.utils.BlockManager.shouldBlock
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import javax.inject.Inject import javax.inject.Inject
abstract class NotificationsListViewModel : abstract class NotificationsListViewModel :
@ -53,9 +71,16 @@ private class NotificationsListPartialChangeProducer(private val type: Notificat
(when (type) { (when (type) {
NotificationsType.ReplyMe -> TiebaApi.getInstance().replyMeFlow() NotificationsType.ReplyMe -> TiebaApi.getInstance().replyMeFlow()
NotificationsType.AtMe -> TiebaApi.getInstance().atMeFlow() NotificationsType.AtMe -> TiebaApi.getInstance().atMeFlow()
}).map<MessageListBean, NotificationsListPartialChange.Refresh> { }).map<MessageListBean, NotificationsListPartialChange.Refresh> { messageListBean ->
val data = (if (type == NotificationsType.ReplyMe) it.replyList else it.atList)!! val data =
NotificationsListPartialChange.Refresh.Success(data = data, hasMore = it.page?.hasMore == "1") ((if (type == NotificationsType.ReplyMe) messageListBean.replyList else messageListBean.atList)
?: emptyList()).fastMap {
MessageItemData(it)
}
NotificationsListPartialChange.Refresh.Success(
data = data,
hasMore = messageListBean.page?.hasMore == "1"
)
} }
.onStart { emit(NotificationsListPartialChange.Refresh.Start) } .onStart { emit(NotificationsListPartialChange.Refresh.Start) }
.catch { emit(NotificationsListPartialChange.Refresh.Failure(it)) } .catch { emit(NotificationsListPartialChange.Refresh.Failure(it)) }
@ -64,9 +89,17 @@ private class NotificationsListPartialChangeProducer(private val type: Notificat
(when (type) { (when (type) {
NotificationsType.ReplyMe -> TiebaApi.getInstance().replyMeFlow(page = page) NotificationsType.ReplyMe -> TiebaApi.getInstance().replyMeFlow(page = page)
NotificationsType.AtMe -> TiebaApi.getInstance().atMeFlow(page = page) NotificationsType.AtMe -> TiebaApi.getInstance().atMeFlow(page = page)
}).map<MessageListBean, NotificationsListPartialChange.LoadMore> { }).map<MessageListBean, NotificationsListPartialChange.LoadMore> { messageListBean ->
val data = (if (type == NotificationsType.ReplyMe) it.replyList else it.atList)!! val data =
NotificationsListPartialChange.LoadMore.Success(currentPage = page, data = data, hasMore = it.page?.hasMore == "1") ((if (type == NotificationsType.ReplyMe) messageListBean.replyList else messageListBean.atList)
?: emptyList()).fastMap {
MessageItemData(it)
}
NotificationsListPartialChange.LoadMore.Success(
currentPage = page,
data = data,
hasMore = messageListBean.page?.hasMore == "1"
)
} }
.onStart { emit(NotificationsListPartialChange.LoadMore.Start) } .onStart { emit(NotificationsListPartialChange.LoadMore.Start) }
.catch { emit(NotificationsListPartialChange.LoadMore.Failure(currentPage = page, error = it)) } .catch { emit(NotificationsListPartialChange.LoadMore.Failure(currentPage = page, error = it)) }
@ -77,7 +110,7 @@ enum class NotificationsType {
} }
sealed interface NotificationsListUiIntent : UiIntent { sealed interface NotificationsListUiIntent : UiIntent {
object Refresh : NotificationsListUiIntent data object Refresh : NotificationsListUiIntent
data class LoadMore(val page: Int) : NotificationsListUiIntent data class LoadMore(val page: Int) : NotificationsListUiIntent
} }
@ -87,14 +120,20 @@ sealed interface NotificationsListPartialChange : PartialChange<NotificationsLis
override fun reduce(oldState: NotificationsListUiState): NotificationsListUiState = override fun reduce(oldState: NotificationsListUiState): NotificationsListUiState =
when (this) { when (this) {
Start -> oldState.copy(isRefreshing = true) Start -> oldState.copy(isRefreshing = true)
is Success -> oldState.copy(isRefreshing = false, currentPage = 1, data = data, hasMore = hasMore) is Success -> oldState.copy(
isRefreshing = false,
currentPage = 1,
data = data.toImmutableList(),
hasMore = hasMore
)
is Failure -> oldState.copy(isRefreshing = false) is Failure -> oldState.copy(isRefreshing = false)
} }
object Start: Refresh() data object Start : Refresh()
data class Success( data class Success(
val data: List<MessageListBean.MessageInfoBean>, val data: List<MessageItemData>,
val hasMore: Boolean, val hasMore: Boolean,
) : Refresh() ) : Refresh()
@ -110,17 +149,17 @@ sealed interface NotificationsListPartialChange : PartialChange<NotificationsLis
is Success -> oldState.copy( is Success -> oldState.copy(
isLoadingMore = false, isLoadingMore = false,
currentPage = currentPage, currentPage = currentPage,
data = oldState.data + data, data = (oldState.data + data).toImmutableList(),
hasMore = hasMore hasMore = hasMore
) )
is Failure -> oldState.copy(isLoadingMore = false) is Failure -> oldState.copy(isLoadingMore = false)
} }
object Start: LoadMore() data object Start : LoadMore()
data class Success( data class Success(
val currentPage: Int, val currentPage: Int,
val data: List<MessageListBean.MessageInfoBean>, val data: List<MessageItemData>,
val hasMore: Boolean, val hasMore: Boolean,
) : LoadMore() ) : LoadMore()
@ -131,12 +170,18 @@ sealed interface NotificationsListPartialChange : PartialChange<NotificationsLis
} }
} }
@Immutable
data class MessageItemData(
val info: MessageListBean.MessageInfoBean,
val blocked: Boolean = info.shouldBlock(),
)
data class NotificationsListUiState( data class NotificationsListUiState(
val isRefreshing: Boolean = true, val isRefreshing: Boolean = true,
val isLoadingMore: Boolean = false, val isLoadingMore: Boolean = false,
val currentPage: Int = 1, val currentPage: Int = 1,
val hasMore: Boolean = true, val hasMore: Boolean = true,
val data: List<MessageListBean.MessageInfoBean> = emptyList(), val data: ImmutableList<MessageItemData> = persistentListOf(),
) : UiState ) : UiState
sealed interface NotificationsListUiEvent : UiEvent sealed interface NotificationsListUiEvent : UiEvent

View File

@ -1,5 +1,6 @@
package com.huanchengfly.tieba.post.utils package com.huanchengfly.tieba.post.utils
import com.huanchengfly.tieba.post.api.models.MessageListBean
import com.huanchengfly.tieba.post.api.models.protos.Post 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.SubPostList
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
@ -64,4 +65,10 @@ object BlockManager {
fun SubPostList.shouldBlock(): Boolean = fun SubPostList.shouldBlock(): Boolean =
shouldBlock(content.plainText) || shouldBlock(author_id, author?.name) shouldBlock(content.plainText) || shouldBlock(author_id, author?.name)
fun MessageListBean.MessageInfoBean.shouldBlock(): Boolean =
shouldBlock(content.orEmpty()) || shouldBlock(
this.replyer?.id?.toLongOrNull() ?: -1,
this.replyer?.name.orEmpty()
)
} }

View File

@ -709,6 +709,7 @@
<string name="btn_open_origin_thread">查看原贴</string> <string name="btn_open_origin_thread">查看原贴</string>
<string name="tip_blocked_thread">由于你的屏蔽设置,该贴已被屏蔽</string> <string name="tip_blocked_thread">由于你的屏蔽设置,该贴已被屏蔽</string>
<string name="tip_blocked_content">由于你的屏蔽设置,该内容已被屏蔽</string> <string name="tip_blocked_content">由于你的屏蔽设置,该内容已被屏蔽</string>
<string name="tip_blocked_message">由于你的屏蔽设置,该消息已被屏蔽</string>
<string name="desc_picture">图片</string> <string name="desc_picture">图片</string>
<string name="tip_thread_store_deleted">该贴已被删除</string> <string name="tip_thread_store_deleted">该贴已被删除</string>
<string name="button_expand">展开</string> <string name="button_expand">展开</string>