feat: 消息列表屏蔽
This commit is contained in:
parent
478f275ceb
commit
6527155925
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue