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,100 +102,120 @@ 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}" },
) { ) {
Column( val (info, blocked) = it
BlockableContent(
blocked = blocked,
blockedTip = {
BlockTip {
Text(
text = stringResource(id = R.string.tip_blocked_message)
)
}
},
modifier = Modifier modifier = Modifier
.clickable { .padding(horizontal = 16.dp, vertical = 12.dp)
if (it.isFloor == "1") { ) {
navigator.navigate( Column(
SubPostsPageDestination( modifier = Modifier
threadId = it.threadId!!.toLong(), .clickable {
subPostId = it.postId!!.toLong(), if (info.isFloor == "1") {
loadFromSubPost = true navigator.navigate(
SubPostsPageDestination(
threadId = info.threadId!!.toLong(),
subPostId = info.postId!!.toLong(),
loadFromSubPost = true
)
) )
) } else {
navigator.navigate(
ThreadPageDestination(
threadId = info.threadId!!.toLong(),
postId = info.postId!!.toLong()
)
)
}
}
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
if (info.replyer != null) {
UserHeader(
avatar = {
Avatar(
data = StringUtil.getAvatarUrl(info.replyer.portrait),
size = Sizes.Small,
contentDescription = null
)
},
name = {
Text(
text = info.replyer.nameShow ?: info.replyer.name ?: ""
)
},
onClick = {
UserActivity.launch(
context,
info.replyer.id!!,
StringUtil.getAvatarUrl(info.replyer.portrait)
)
},
desc = {
Text(
text = DateTimeUtils.getRelativeTimeString(
LocalContext.current,
info.time!!
)
)
},
) {}
}
EmoticonText(text = info.content ?: "")
val quoteText = if (type == NotificationsType.ReplyMe) {
if ("1" == info.isFloor) {
info.quoteContent
} else { } else {
navigator.navigate( stringResource(
ThreadPageDestination( id = R.string.text_message_list_item_reply_my_thread,
threadId = it.threadId!!.toLong(), info.title ?: ""
postId = it.postId!!.toLong()
)
) )
} }
}
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
if (it.replyer != null) {
UserHeader(
avatar = {
Avatar(
data = StringUtil.getAvatarUrl(it.replyer.portrait),
size = Sizes.Small,
contentDescription = null
)
},
name = {
Text(
text = it.replyer.nameShow ?: it.replyer.name ?: ""
)
},
onClick = {
UserActivity.launch(
context,
it.replyer.id!!,
StringUtil.getAvatarUrl(it.replyer.portrait)
)
},
desc = {
Text(
text = DateTimeUtils.getRelativeTimeString(
LocalContext.current,
it.time!!
)
)
},
) {}
}
EmoticonText(text = it.content ?: "")
val quoteText = if (type == NotificationsType.ReplyMe) {
if ("1" == it.isFloor) {
it.quoteContent
} else { } else {
stringResource(id = R.string.text_message_list_item_reply_my_thread, it.title ?: "") info.title
} }
} else { if (quoteText != null) {
it.title EmoticonText(
} text = quoteText,
if (quoteText != null) { modifier = Modifier
EmoticonText( .fillMaxWidth()
text = quoteText, .clip(RoundedCornerShape(6.dp))
modifier = Modifier .clickable {
.fillMaxWidth() if ("1" == info.isFloor && info.quotePid != null) {
.clip(RoundedCornerShape(6.dp)) navigator.navigate(
.clickable { SubPostsPageDestination(
if ("1" == it.isFloor && it.quotePid != null) { threadId = info.threadId!!.toLong(),
navigator.navigate( postId = info.quotePid.toLong(),
SubPostsPageDestination( loadFromSubPost = true,
threadId = it.threadId!!.toLong(), )
postId = it.quotePid.toLong(),
loadFromSubPost = true,
) )
) } else {
} else { navigator.navigate(
navigator.navigate( ThreadPageDestination(
ThreadPageDestination( threadId = info.threadId!!.toLong(),
threadId = it.threadId!!.toLong(), )
) )
) }
} }
} .background(
.background(ExtendedTheme.colors.chip, RoundedCornerShape(6.dp)) ExtendedTheme.colors.chip,
.padding(8.dp), RoundedCornerShape(6.dp)
color = ExtendedTheme.colors.onChip, )
fontSize = 12.sp, .padding(8.dp),
) color = ExtendedTheme.colors.onChip,
fontSize = 12.sp,
)
}
} }
} }
} }

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>