更改推送

This commit is contained in:
2024-09-20 17:10:54 +08:00
parent 93182c3985
commit 054dd68b89
20 changed files with 390 additions and 138 deletions

View File

@@ -13,6 +13,8 @@ data class ActionExtra(
val action: String,
@SerializedName("postId")
val postId: String?,
@SerializedName("commentId")
val commentId: String?
)
class JpushReciver : JPushMessageReceiver() {
@@ -30,12 +32,18 @@ class JpushReciver : JPushMessageReceiver() {
val actionExtra = message.notificationExtras?.let {
gson.fromJson(it, ActionExtra::class.java)
}
actionExtra?.postId?.let {
val intent = Intent(context, MainActivity::class.java).apply {
putExtra("POST_ID", it)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
actionExtra?.postId?.let {
intent.putExtra("POST_ID", it)
}
actionExtra?.commentId?.let {
intent.putExtra("COMMENT_ID", it)
}
actionExtra?.action?.let {
intent.putExtra("ACTION", it)
}
context?.startActivity(intent)
}
}
}

View File

@@ -29,6 +29,7 @@ import com.aiosman.riderpro.data.AccountServiceImpl
import com.aiosman.riderpro.ui.Navigation
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.post.NewPostViewModel
import com.aiosman.riderpro.utils.Utils
import com.google.android.libraries.places.api.Places
import com.google.firebase.analytics.FirebaseAnalytics
import kotlinx.coroutines.CoroutineScope
@@ -58,6 +59,9 @@ class MainActivity : ComponentActivity() {
val accountService: AccountService = AccountServiceImpl()
try {
val resp = accountService.getMyAccount()
accountService.updateUserLanguage(
Utils.getCurrentLanguage()
)
// 设置当前登录用户 ID
AppState.UserId = resp.id
return true
@@ -112,13 +116,25 @@ class MainActivity : ComponentActivity() {
Navigation(startDestination) { navController ->
// 处理带有 postId 的通知点击
val postId = intent.getStringExtra("POST_ID")
var commentId = intent.getStringExtra("COMMENT_ID")
var action = intent.getStringExtra("ACTION")
if (action == "newFollow") {
navController.navigate(NavigationRoute.Followers.route)
return@Navigation
}
if (commentId == null) {
commentId = "0"
}
if (postId != null) {
Log.d("MainActivity", "Navigation to Post$postId")
navController.navigate(
NavigationRoute.Post.route.replace(
NavigationRoute.Post.route
.replace(
"{id}",
postId
)
.replace("{highlightCommentId}", commentId)
)
}
// 处理分享过来的图片

View File

@@ -8,6 +8,7 @@ import com.aiosman.riderpro.data.api.RegisterMessageChannelRequestBody
import com.aiosman.riderpro.data.api.RegisterRequestBody
import com.aiosman.riderpro.data.api.ResetPasswordRequestBody
import com.aiosman.riderpro.data.api.UpdateNoticeRequestBody
import com.aiosman.riderpro.data.api.UpdateUserLangRequestBody
import com.aiosman.riderpro.entity.AccountFavouriteEntity
import com.aiosman.riderpro.entity.AccountLikeEntity
import com.aiosman.riderpro.entity.AccountProfileEntity
@@ -339,10 +340,20 @@ interface AccountService {
*/
suspend fun updateNotice(payload: UpdateNoticeRequestBody)
/**
* 注册消息通道
*/
suspend fun registerMessageChannel(client: String, identifier: String)
/**
* 重置密码
*/
suspend fun resetPassword(email: String)
/**
* 更新用户语言
*/
suspend fun updateUserLanguage(language: String)
}
class AccountServiceImpl : AccountService {
@@ -473,4 +484,8 @@ class AccountServiceImpl : AccountService {
}
}
override suspend fun updateUserLanguage(language: String) {
ApiClient.api.updateUserLang(UpdateUserLangRequestBody(language))
}
}

View File

@@ -68,6 +68,13 @@ interface CommentService {
* @param commentId 评论ID
*/
suspend fun DeleteComment(commentId: Int)
/**
* 获取评论
* @param commentId 评论ID
*/
suspend fun getCommentById(commentId: Int): CommentEntity
}
/**
@@ -120,7 +127,6 @@ data class Comment(
comment = content,
date = ApiClient.dateFromApiString(createdAt),
likes = likeCount,
replies = emptyList(),
postId = postId,
avatar = "${ApiClient.BASE_SERVER}${user.avatar}",
author = user.id,
@@ -240,4 +246,10 @@ class CommentServiceImpl : CommentService {
val resp = ApiClient.api.deleteComment(commentId)
return
}
override suspend fun getCommentById(commentId: Int): CommentEntity {
val resp = ApiClient.api.getComment(commentId)
val body = resp.body() ?: throw ServiceException("Failed to get comment")
return body.data.toCommentEntity()
}
}

View File

@@ -98,6 +98,11 @@ data class ResetPasswordRequestBody(
val username: String,
)
data class UpdateUserLangRequestBody(
@SerializedName("language")
val lang: String,
)
interface RiderProAPI {
@POST("register")
suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
@@ -280,4 +285,15 @@ interface RiderProAPI {
suspend fun resetPassword(
@Body body: ResetPasswordRequestBody
): Response<Unit>
@GET("comment/{id}")
suspend fun getComment(
@Path("id") id: Int
): Response<DataContainer<Comment>>
@PATCH("account/my/lang")
suspend fun updateUserLang(
@Body body: UpdateUserLangRequestBody
): Response<Unit>
}

View File

@@ -13,7 +13,6 @@ data class CommentEntity(
val comment: String,
val date: Date,
val likes: Int,
val replies: List<CommentEntity>,
val postId: Int = 0,
val avatar: String,
val author: Long,

View File

@@ -61,7 +61,7 @@ sealed class NavigationRoute(
data object LocationDetail : NavigationRoute("LocationDetail/{x}/{y}")
data object OfficialPhoto : NavigationRoute("OfficialPhoto")
data object OfficialPhotographer : NavigationRoute("OfficialPhotographer")
data object Post : NavigationRoute("Post/{id}")
data object Post : NavigationRoute("Post/{id}/{highlightCommentId}")
data object ModificationList : NavigationRoute("ModificationList")
data object MyMessage : NavigationRoute("MyMessage")
data object Comments : NavigationRoute("Comments")
@@ -135,14 +135,21 @@ fun NavigationController(
}
composable(
route = NavigationRoute.Post.route,
arguments = listOf(navArgument("id") { type = NavType.StringType }),
arguments = listOf(
navArgument("id") { type = NavType.StringType },
navArgument("highlightCommentId") { type = NavType.IntType }
),
) { backStackEntry ->
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
val id = backStackEntry.arguments?.getString("id")
val highlightCommentId = backStackEntry.arguments?.getInt("highlightCommentId")?.let {
if (it == 0) null else it
}
PostScreen(
id!!
id!!,
highlightCommentId
)
}
}

View File

@@ -78,7 +78,7 @@ fun FavouriteListPage() {
NavigationRoute.Post.route.replace(
"{id}",
momentItem.id.toString()
)
).replace("{highlightCommentId}", "0")
)
}
) {

View File

@@ -112,7 +112,6 @@ fun NotificationsScreen() {
// 清除点赞消息数量
MessageListViewModel.clearLikeNoticeCount()
}
navController.navigate(NavigationRoute.Likes.route)
}
NotificationIndicator(
@@ -147,8 +146,7 @@ fun NotificationsScreen() {
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(bottom = 48.dp)
,
.padding(bottom = 48.dp),
contentAlignment = Alignment.Center
) {
Column(
@@ -177,10 +175,17 @@ fun NotificationsScreen() {
CommentNoticeItem(comment) {
MessageListViewModel.updateReadStatus(comment.id)
MessageListViewModel.viewModelScope.launch {
var highlightCommentId = comment.id
comment.parentCommentId?.let {
highlightCommentId = it
}
navController.navigate(
NavigationRoute.Post.route.replace(
"{id}",
comment.postId.toString()
).replace(
"{highlightCommentId}",
highlightCommentId.toString()
)
)
}
@@ -387,8 +392,12 @@ fun CommentNoticeItem(
)
Spacer(modifier = Modifier.height(4.dp))
Row {
var text = commentItem.comment
if (commentItem.parentCommentId != null) {
text = "Reply you: $text"
}
Text(
text = commentItem.comment,
text = text,
fontSize = 14.sp,
maxLines = 1,
color = Color(0x99000000),
@@ -406,6 +415,10 @@ fun CommentNoticeItem(
}
Spacer(modifier = Modifier.width(24.dp))
commentItem.post?.let {
Box {
Box(
modifier = Modifier.padding(4.dp)
) {
CustomAsyncImage(
context = context,
imageUrl = it.images[0].thumbnail,
@@ -413,6 +426,21 @@ fun CommentNoticeItem(
modifier = Modifier
.size(48.dp)
)
// unread indicator
}
if (commentItem.unread) {
Box(
modifier = Modifier
.background(Color(0xFFE53935), CircleShape)
.size(12.dp)
.align(Alignment.TopEnd)
)
}
}
}
}

View File

@@ -85,6 +85,7 @@ object MessageListViewModel : ViewModel() {
viewModelScope.launch {
commentService.updateReadStatus(id)
updateIsRead(id)
updateUnReadCount(-1)
}
}
@@ -98,4 +99,9 @@ object MessageListViewModel : ViewModel() {
fun clearFavouriteNoticeCount() {
noticeInfo = noticeInfo?.copy(favoriteCount = 0)
}
fun updateUnReadCount(delta : Int) {
noticeInfo?.let {
noticeInfo = it.copy(commentCount = it.commentCount + delta)
}
}
}

View File

@@ -175,8 +175,11 @@ fun MomentCard(
modifier = Modifier
.fillMaxWidth()
.noRippleClickable {
// PostViewModel.preTransit(momentEntity)
navController.navigate("Post/${momentEntity.id}")
navController.navigate(
route = NavigationRoute.Post.route
.replace("{id}", momentEntity.id.toString())
.replace("{highlightCommentId}", "0")
)
}
) {
MomentContentGroup(

View File

@@ -813,7 +813,7 @@ fun MomentCardPicture(imageUrl: String, momentEntity: MomentEntity) {
NavigationRoute.Post.route.replace(
"{id}",
momentEntity.id.toString()
)
).replace("{highlightCommentId}", "0")
)
},
contentDescription = "",

View File

@@ -163,7 +163,7 @@ fun DiscoverView() {
NavigationRoute.Post.route.replace(
"{id}",
momentItem.id.toString()
)
).replace("{highlightCommentId}", "0")
)
}
) {

View File

@@ -158,7 +158,7 @@ fun ActionPostNoticeItem(
NavigationRoute.Post.route.replace(
"{id}",
postId.toString()
)
).replace("{highlightCommentId}", "0")
)
}
) {
@@ -196,7 +196,7 @@ fun LikeCommentNoticeItem(
NavigationRoute.Post.route.replace(
"{id}",
it.toString()
)
).replace("{highlightCommentId}", "0")
)
}

View File

@@ -38,6 +38,7 @@ import com.aiosman.riderpro.ui.composables.ActionButton
import com.aiosman.riderpro.ui.composables.CheckboxWithLabel
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.composables.TextInputField
import com.aiosman.riderpro.utils.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -155,6 +156,9 @@ fun EmailSignupScreen() {
try {
val resp = accountService.getMyAccount()
AppState.UserId = resp.id
accountService.updateUserLanguage(
Utils.getCurrentLanguage()
)
Messaging.RegistDevice(scope, context)
} catch (e: ServiceException) {
scope.launch(Dispatchers.Main) {

View File

@@ -40,6 +40,7 @@ import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.ActionButton
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.utils.GoogleLogin
import com.aiosman.riderpro.utils.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -84,6 +85,9 @@ fun SignupScreen() {
// 获取token 信息
try {
val resp = accountService.getMyAccount()
accountService.updateUserLanguage(
Utils.getCurrentLanguage()
)
AppState.UserId = resp.id
Messaging.RegistDevice(coroutineScope, context)
} catch (e: ServiceException) {

View File

@@ -31,6 +31,8 @@ class CommentsViewModel(
var order: String by mutableStateOf("like")
var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList())
var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>())
var highlightCommentId by mutableStateOf<Int?>(null)
var highlightComment by mutableStateOf<CommentEntity?>(null)
/**
* 预加载,在跳转到 PostScreen 之前设置好内容
@@ -49,6 +51,9 @@ class CommentsViewModel(
}
}
/**
* 加载评论
*/
fun reloadComment() {
viewModelScope.launch {
try {
@@ -68,18 +73,75 @@ class CommentsViewModel(
}
}
suspend fun likeComment(commentId: Int) {
commentService.likeComment(commentId)
suspend fun highlightComment(commentId: Int) {
highlightCommentId = commentId
val resp = commentService.getCommentById(commentId)
highlightComment = resp
}
/**
* 更新高亮评论点赞状态
*/
private fun updateHighlightCommentLike(commentId: Int, isLike: Boolean): Boolean {
var isUpdate = false
highlightComment?.let {
if (it.id == commentId) {
highlightComment =
it.copy(liked = isLike, likes = if (isLike) it.likes + 1 else it.likes - 1)
isUpdate = true
}
highlightComment = it.copy(
reply = it.reply.map { replyComment ->
if (replyComment.id == commentId) {
isUpdate = true
replyComment.copy(
liked = isLike,
likes = if (isLike) replyComment.likes + 1 else replyComment.likes - 1
)
} else {
replyComment
}
}
)
}
return isUpdate
}
/**
* 更新添加的评论点赞状态
*/
private fun updateAddedCommentLike(commentId: Int, isLike: Boolean): Boolean {
var isUpdate = false
addedCommentList = addedCommentList.map {
if (it.id == commentId) {
isUpdate = true
it.copy(liked = isLike, likes = if (isLike) it.likes + 1 else it.likes - 1)
} else {
it
}
}
return isUpdate
}
/**
* 更新评论点赞状态
*/
private fun updateCommentLike(commentId: Int, isLike: Boolean) {
val currentPagingData = commentsFlow.value
val updatedPagingData = currentPagingData.map { comment ->
if (comment.id == commentId) {
comment.copy(liked = !comment.liked, likes = comment.likes + 1)
comment.copy(
liked = isLike,
likes = if (isLike) comment.likes + 1 else comment.likes - 1
)
} else {
// 可能是回复的评论
comment.copy(reply = comment.reply.map { replyComment ->
if (replyComment.id == commentId) {
replyComment.copy(
liked = !replyComment.liked, likes = replyComment.likes + 1
liked = isLike,
likes = if (isLike) replyComment.likes + 1 else replyComment.likes - 1
)
} else {
replyComment
@@ -88,45 +150,40 @@ class CommentsViewModel(
}
}
_commentsFlow.value = updatedPagingData
// 更新addCommentList
addedCommentList = addedCommentList.map {
if (it.id == commentId) {
it.copy(liked = !it.liked, likes = it.likes + 1)
} else {
it
}
/**
* 点赞评论
*/
suspend fun likeComment(commentId: Int) {
try {
commentService.likeComment(commentId)
// 更新addCommentList
if (updateHighlightCommentLike(commentId, true)) {
return
}
if (updateAddedCommentLike(commentId, true)) {
return
}
updateCommentLike(commentId, true)
} catch (e: Exception) {
e.printStackTrace()
}
}
// 取消点赞评论
suspend fun unlikeComment(commentId: Int) {
commentService.dislikeComment(commentId)
val currentPagingData = commentsFlow.value
val updatedPagingData = currentPagingData.map { comment ->
if (comment.id == commentId) {
comment.copy(liked = !comment.liked, likes = comment.likes - 1)
} else {
// 可能是回复的评论
comment.copy(reply = comment.reply.map { replyComment ->
if (replyComment.id == commentId) {
replyComment.copy(
liked = !replyComment.liked, likes = replyComment.likes - 1
)
} else {
replyComment
}
})
}
}
_commentsFlow.value = updatedPagingData
// 更新addCommentList
addedCommentList = addedCommentList.map {
if (it.id == commentId) {
it.copy(liked = !it.liked, likes = it.likes - 1)
} else {
it
// 更新高亮评论点赞状态
if (updateHighlightCommentLike(commentId, false)) {
return
}
// 更新添加的评论点赞状态
if (updateAddedCommentLike(commentId, false)) {
return
}
// 更新评论点赞状态
updateCommentLike(commentId, false)
}
suspend fun createComment(
@@ -163,6 +220,24 @@ class CommentsViewModel(
}
fun loadMoreSubComments(commentId: Int) {
if (highlightComment?.id == commentId) {
// 高亮的评论,更新高亮评论的回复
highlightComment?.let {
viewModelScope.launch {
val subCommentList = commentService.getComments(
postId = postId.toInt(),
parentCommentId = commentId,
pageNumber = it.replyPage + 1,
pageSize = 3,
).list
highlightComment = it.copy(
reply = it.reply.plus(subCommentList),
replyPage = it.replyPage + 1
)
}
}
} else {
// 普通评论
viewModelScope.launch {
val currentPagingData = commentsFlow.value
val updatedPagingData = currentPagingData.map { comment ->
@@ -174,6 +249,7 @@ class CommentsViewModel(
parentCommentId = commentId,
pageNumber = comment.replyPage + 1,
pageSize = 3,
order = "earliest"
).list
return@map comment.copy(
reply = comment.reply.plus(subCommentList),
@@ -190,4 +266,6 @@ class CommentsViewModel(
_commentsFlow.value = updatedPagingData
}
}
}
}

View File

@@ -3,7 +3,6 @@ package com.aiosman.riderpro.ui.post
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -28,7 +27,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
@@ -51,7 +49,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.focusModifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
@@ -103,6 +100,7 @@ import kotlinx.coroutines.launch
@Composable
fun PostScreen(
id: String,
highlightCommentId: Int?
) {
val viewModel = viewModel<PostViewModel>(
key = "PostViewModel_$id",
@@ -124,7 +122,11 @@ fun PostScreen(
skipPartiallyExpanded = true
)
LaunchedEffect(Unit) {
viewModel.initData()
viewModel.initData(
highlightCommentId = highlightCommentId.let {
if (it == 0) null else it
}
)
}
if (showCommentMenu) {
@@ -147,7 +149,6 @@ fun PostScreen(
contextComment?.let {
viewModel.deleteComment(it.id)
}
},
commentEntity = contextComment,
onCloseClick = {
@@ -405,7 +406,44 @@ fun CommentContent(
val addedTopLevelComment = viewModel.addedCommentList.filter {
it.parentCommentId == null
}
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.animateContentSize(animationSpec = tween(durationMillis = 500))
) {
viewModel.highlightComment?.let {
CommentItem(
it,
onLike = { comment ->
viewModel.viewModelScope.launch {
if (comment.liked) {
viewModel.unlikeComment(comment.id)
} else {
viewModel.likeComment(comment.id)
}
}
},
onLongClick = { comment ->
onLongClick(comment)
},
onReply = { parentComment, _, _, _ ->
onReply(
parentComment,
parentComment.author,
parentComment.name,
parentComment.avatar
)
},
onLoadMoreSubComments = {
viewModel.viewModelScope.launch {
viewModel.loadMoreSubComments(it.id)
}
},
)
}
}
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
@@ -454,6 +492,7 @@ fun CommentContent(
for (idx in 0 until commentsPagging.itemCount) {
val item = commentsPagging[idx] ?: return
if (item.id != viewModel.highlightCommentId) {
AnimatedVisibility(
visible = true,
enter = slideInVertically(),
@@ -495,6 +534,8 @@ fun CommentContent(
}
}
}
// Handle loading and error states as before
if (commentsPagging.loadState.refresh is LoadState.Loading) {
Box(
@@ -932,7 +973,9 @@ fun CommentItem(
}
Spacer(modifier = Modifier.height(8.dp))
Column(
modifier = Modifier.padding(start = 12.dp + 40.dp).animateContentSize(
modifier = Modifier
.padding(start = 12.dp + 40.dp)
.animateContentSize(
animationSpec = tween(
durationMillis = 500
)

View File

@@ -29,20 +29,28 @@ class PostViewModel(
var accountService: AccountService = AccountServiceImpl()
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId)
var isError by mutableStateOf(false)
var isFirstLoad by mutableStateOf(true)
fun reloadComment() {
commentsViewModel.reloadComment()
}
suspend fun initData() {
suspend fun initData(highlightCommentId: Int? = null) {
if (!isFirstLoad) {
return
}
isFirstLoad = false
try {
moment = service.getMomentById(postId.toInt())
} catch (e: Exception) {
isError = true
return
}
highlightCommentId?.let {
commentsViewModel.highlightComment(it)
}
commentsViewModel.reloadComment()
}
suspend fun likeComment(commentId: Int) {

View File

@@ -8,6 +8,7 @@ import coil.request.CachePolicy
import com.aiosman.riderpro.data.api.AuthInterceptor
import com.aiosman.riderpro.data.api.getUnsafeOkHttpClient
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
object Utils {
@@ -56,4 +57,8 @@ object Utils {
else -> "$years years ago"
}
}
fun getCurrentLanguage(): String {
return Locale.getDefault().language
}
}