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()
?: persistentListOf()
@OptIn(ExperimentalTextApi::class)
fun SubPostList.getContentText(threadAuthorId: Long? = null): AnnotatedString {
val context = App.INSTANCE
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.ThreadPageDestination
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.LazyLoad
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.utils.DateTimeUtils
import com.huanchengfly.tieba.post.utils.StringUtil
import kotlinx.collections.immutable.persistentListOf
@OptIn(ExperimentalMaterialApi::class)
@Composable
@ -73,7 +76,7 @@ fun NotificationsListPage(
)
val data by viewModel.uiState.collectPartialAsState(
prop1 = NotificationsListUiState::data,
initial = emptyList()
initial = persistentListOf()
)
val currentPage by viewModel.uiState.collectPartialAsState(
prop1 = NotificationsListUiState::currentPage,
@ -99,100 +102,120 @@ fun NotificationsListPage(
) {
items(
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
.clickable {
if (it.isFloor == "1") {
navigator.navigate(
SubPostsPageDestination(
threadId = it.threadId!!.toLong(),
subPostId = it.postId!!.toLong(),
loadFromSubPost = true
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Column(
modifier = Modifier
.clickable {
if (info.isFloor == "1") {
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 {
navigator.navigate(
ThreadPageDestination(
threadId = it.threadId!!.toLong(),
postId = it.postId!!.toLong()
)
stringResource(
id = R.string.text_message_list_item_reply_my_thread,
info.title ?: ""
)
}
}
.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 {
stringResource(id = R.string.text_message_list_item_reply_my_thread, it.title ?: "")
info.title
}
} else {
it.title
}
if (quoteText != null) {
EmoticonText(
text = quoteText,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(6.dp))
.clickable {
if ("1" == it.isFloor && it.quotePid != null) {
navigator.navigate(
SubPostsPageDestination(
threadId = it.threadId!!.toLong(),
postId = it.quotePid.toLong(),
loadFromSubPost = true,
if (quoteText != null) {
EmoticonText(
text = quoteText,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(6.dp))
.clickable {
if ("1" == info.isFloor && info.quotePid != null) {
navigator.navigate(
SubPostsPageDestination(
threadId = info.threadId!!.toLong(),
postId = info.quotePid.toLong(),
loadFromSubPost = true,
)
)
)
} else {
navigator.navigate(
ThreadPageDestination(
threadId = it.threadId!!.toLong(),
} else {
navigator.navigate(
ThreadPageDestination(
threadId = info.threadId!!.toLong(),
)
)
)
}
}
}
.background(ExtendedTheme.colors.chip, RoundedCornerShape(6.dp))
.padding(8.dp),
color = ExtendedTheme.colors.onChip,
fontSize = 12.sp,
)
.background(
ExtendedTheme.colors.chip,
RoundedCornerShape(6.dp)
)
.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
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.util.fastMap
import com.huanchengfly.tieba.post.api.TiebaApi
import com.huanchengfly.tieba.post.api.models.MessageListBean
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
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
abstract class NotificationsListViewModel :
@ -53,9 +71,16 @@ private class NotificationsListPartialChangeProducer(private val type: Notificat
(when (type) {
NotificationsType.ReplyMe -> TiebaApi.getInstance().replyMeFlow()
NotificationsType.AtMe -> TiebaApi.getInstance().atMeFlow()
}).map<MessageListBean, NotificationsListPartialChange.Refresh> {
val data = (if (type == NotificationsType.ReplyMe) it.replyList else it.atList)!!
NotificationsListPartialChange.Refresh.Success(data = data, hasMore = it.page?.hasMore == "1")
}).map<MessageListBean, NotificationsListPartialChange.Refresh> { messageListBean ->
val data =
((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) }
.catch { emit(NotificationsListPartialChange.Refresh.Failure(it)) }
@ -64,9 +89,17 @@ private class NotificationsListPartialChangeProducer(private val type: Notificat
(when (type) {
NotificationsType.ReplyMe -> TiebaApi.getInstance().replyMeFlow(page = page)
NotificationsType.AtMe -> TiebaApi.getInstance().atMeFlow(page = page)
}).map<MessageListBean, NotificationsListPartialChange.LoadMore> {
val data = (if (type == NotificationsType.ReplyMe) it.replyList else it.atList)!!
NotificationsListPartialChange.LoadMore.Success(currentPage = page, data = data, hasMore = it.page?.hasMore == "1")
}).map<MessageListBean, NotificationsListPartialChange.LoadMore> { messageListBean ->
val data =
((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) }
.catch { emit(NotificationsListPartialChange.LoadMore.Failure(currentPage = page, error = it)) }
@ -77,7 +110,7 @@ enum class NotificationsType {
}
sealed interface NotificationsListUiIntent : UiIntent {
object Refresh : NotificationsListUiIntent
data object Refresh : NotificationsListUiIntent
data class LoadMore(val page: Int) : NotificationsListUiIntent
}
@ -87,14 +120,20 @@ sealed interface NotificationsListPartialChange : PartialChange<NotificationsLis
override fun reduce(oldState: NotificationsListUiState): NotificationsListUiState =
when (this) {
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)
}
object Start: Refresh()
data object Start : Refresh()
data class Success(
val data: List<MessageListBean.MessageInfoBean>,
val data: List<MessageItemData>,
val hasMore: Boolean,
) : Refresh()
@ -110,17 +149,17 @@ sealed interface NotificationsListPartialChange : PartialChange<NotificationsLis
is Success -> oldState.copy(
isLoadingMore = false,
currentPage = currentPage,
data = oldState.data + data,
data = (oldState.data + data).toImmutableList(),
hasMore = hasMore
)
is Failure -> oldState.copy(isLoadingMore = false)
}
object Start: LoadMore()
data object Start : LoadMore()
data class Success(
val currentPage: Int,
val data: List<MessageListBean.MessageInfoBean>,
val data: List<MessageItemData>,
val hasMore: Boolean,
) : 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(
val isRefreshing: Boolean = true,
val isLoadingMore: Boolean = false,
val currentPage: Int = 1,
val hasMore: Boolean = true,
val data: List<MessageListBean.MessageInfoBean> = emptyList(),
val data: ImmutableList<MessageItemData> = persistentListOf(),
) : UiState
sealed interface NotificationsListUiEvent : UiEvent

View File

@ -1,5 +1,6 @@
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.SubPostList
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
@ -64,4 +65,10 @@ object BlockManager {
fun SubPostList.shouldBlock(): Boolean =
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="tip_blocked_thread">由于你的屏蔽设置,该贴已被屏蔽</string>
<string name="tip_blocked_content">由于你的屏蔽设置,该内容已被屏蔽</string>
<string name="tip_blocked_message">由于你的屏蔽设置,该消息已被屏蔽</string>
<string name="desc_picture">图片</string>
<string name="tip_thread_store_deleted">该贴已被删除</string>
<string name="button_expand">展开</string>