feat: Implement moment follow/unfollow feature

This commit implements the follow/unfollow feature for moments in the Search, Timeline, and Explore tabs.

It includes:

- Adding a follow button to moment cards and implementing follow/unfollow logic in the respective ViewModels.
- Updating the UI to reflect the follow status changes in the moment list.
- Handling follow/unfollow API requests.
This commit is contained in:
2024-10-26 17:44:51 +08:00
parent fe2bd2f382
commit 01fb092e83
7 changed files with 129 additions and 13 deletions

View File

@@ -54,6 +54,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.riderpro.AppColors import com.aiosman.riderpro.AppColors
import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.entity.MomentEntity import com.aiosman.riderpro.entity.MomentEntity
@@ -70,17 +71,20 @@ fun MomentCard(
onLikeClick: () -> Unit = {}, onLikeClick: () -> Unit = {},
onFavoriteClick: () -> Unit = {}, onFavoriteClick: () -> Unit = {},
onAddComment: () -> Unit = {}, onAddComment: () -> Unit = {},
onFollowClick: () -> Unit = {},
hideAction: Boolean = false hideAction: Boolean = false
) { ) {
var imageIndex by remember { mutableStateOf(0) } var imageIndex by remember { mutableStateOf(0) }
val navController = LocalNavController.current val navController = LocalNavController.current
Column( Column(
modifier = Modifier.fillMaxWidth().background(AppColors.background) modifier = Modifier
.fillMaxWidth()
.background(AppColors.background)
) { ) {
Box( Box(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp) modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
) { ) {
MomentTopRowGroup(momentEntity = momentEntity) MomentTopRowGroup(momentEntity = momentEntity, onFollowClick = onFollowClick)
} }
Column( Column(
modifier = Modifier modifier = Modifier
@@ -163,9 +167,9 @@ fun ModificationListHeader() {
} }
@Composable @Composable
fun MomentName(name: String) { fun MomentName(name: String, modifier: Modifier = Modifier) {
Text( Text(
modifier = Modifier, modifier = modifier,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
text = name, text = name,
color = AppColors.text, color = AppColors.text,
@@ -214,7 +218,10 @@ fun MomentPostTime(time: String) {
} }
@Composable @Composable
fun MomentTopRowGroup(momentEntity: MomentEntity) { fun MomentTopRowGroup(
momentEntity: MomentEntity,
onFollowClick: () -> Unit = {}
) {
val navController = LocalNavController.current val navController = LocalNavController.current
val context = LocalContext.current val context = LocalContext.current
Row( Row(
@@ -239,7 +246,7 @@ fun MomentTopRowGroup(momentEntity: MomentEntity) {
) )
Column( Column(
modifier = Modifier modifier = Modifier
.defaultMinSize() .weight(1f)
.padding(start = 12.dp, end = 12.dp) .padding(start = 12.dp, end = 12.dp)
) { ) {
Row( Row(
@@ -248,8 +255,13 @@ fun MomentTopRowGroup(momentEntity: MomentEntity) {
.height(22.dp), .height(22.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
MomentName(momentEntity.nickname) MomentName(
modifier = Modifier.weight(1f),
name = momentEntity.nickname
)
// MomentFollowBtn() // MomentFollowBtn()
Spacer(modifier = Modifier.width(16.dp))
} }
Row( Row(
modifier = Modifier modifier = Modifier
@@ -262,6 +274,16 @@ fun MomentTopRowGroup(momentEntity: MomentEntity) {
MomentPostLocation(momentEntity.location) MomentPostLocation(momentEntity.location)
} }
} }
Spacer(modifier = Modifier.width(16.dp))
if (AppState.UserId != momentEntity.authorId) {
FollowButton(
isFollowing = momentEntity.followStatus
) {
onFollowClick()
}
}
} }
} }

View File

@@ -72,7 +72,10 @@ fun ExploreMomentsList() {
model.favoriteMoment(momentItem.id) model.favoriteMoment(momentItem.id)
} }
} }
} },
onFollowClick = {
model.followAction(momentItem)
},
) )
} }
} }

View File

@@ -11,7 +11,9 @@ import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
import androidx.paging.map import androidx.paging.map
import com.aiosman.riderpro.AppState import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.data.Moment
import com.aiosman.riderpro.data.MomentService import com.aiosman.riderpro.data.MomentService
import com.aiosman.riderpro.data.UserServiceImpl
import com.aiosman.riderpro.entity.MomentEntity import com.aiosman.riderpro.entity.MomentEntity
import com.aiosman.riderpro.entity.MomentPagingSource import com.aiosman.riderpro.entity.MomentPagingSource
import com.aiosman.riderpro.entity.MomentRemoteDataSource import com.aiosman.riderpro.entity.MomentRemoteDataSource
@@ -24,6 +26,7 @@ import kotlinx.coroutines.launch
object MomentExploreViewModel : ViewModel() { object MomentExploreViewModel : ViewModel() {
private val momentService: MomentService = MomentServiceImpl() private val momentService: MomentService = MomentServiceImpl()
private val userService = UserServiceImpl()
private val _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty()) private val _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
val momentsFlow = _momentsFlow.asStateFlow() val momentsFlow = _momentsFlow.asStateFlow()
var existsMoment = mutableStateOf(false) var existsMoment = mutableStateOf(false)
@@ -147,6 +150,31 @@ object MomentExploreViewModel : ViewModel() {
momentService.unfavoriteMoment(id) momentService.unfavoriteMoment(id)
updateUnfavoriteCount(id) updateUnfavoriteCount(id)
} }
fun updateFollowStatus(authorId:Int,isFollow:Boolean) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.authorId == authorId) {
momentItem.copy(followStatus = isFollow)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
fun followAction(moment: MomentEntity) {
viewModelScope.launch {
try {
if (moment.followStatus) {
userService.unFollowUser(moment.authorId.toString())
} else {
userService.followUser(moment.authorId.toString())
}
updateFollowStatus(moment.authorId, !moment.followStatus)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun ResetModel() { fun ResetModel() {
_momentsFlow.value = PagingData.empty() _momentsFlow.value = PagingData.empty()

View File

@@ -71,6 +71,9 @@ fun TimelineMomentsList() {
model.favoriteMoment(momentItem.id) model.favoriteMoment(momentItem.id)
} }
} }
},
onFollowClick = {
model.followAction(momentItem)
} }
) )
// Box( // Box(

View File

@@ -13,6 +13,8 @@ import androidx.paging.filter
import androidx.paging.map import androidx.paging.map
import com.aiosman.riderpro.AppState import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.data.MomentService import com.aiosman.riderpro.data.MomentService
import com.aiosman.riderpro.data.UserService
import com.aiosman.riderpro.data.UserServiceImpl
import com.aiosman.riderpro.entity.MomentEntity import com.aiosman.riderpro.entity.MomentEntity
import com.aiosman.riderpro.entity.MomentPagingSource import com.aiosman.riderpro.entity.MomentPagingSource
import com.aiosman.riderpro.entity.MomentRemoteDataSource import com.aiosman.riderpro.entity.MomentRemoteDataSource
@@ -26,6 +28,7 @@ import kotlinx.coroutines.launch
object TimelineMomentViewModel : ViewModel() { object TimelineMomentViewModel : ViewModel() {
private val momentService: MomentService = MomentServiceImpl() private val momentService: MomentService = MomentServiceImpl()
private val _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty()) private val _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
private val userService :UserService = UserServiceImpl()
val momentsFlow = _momentsFlow.asStateFlow() val momentsFlow = _momentsFlow.asStateFlow()
var existsMoment = mutableStateOf(false) var existsMoment = mutableStateOf(false)
var refreshing by mutableStateOf(false) var refreshing by mutableStateOf(false)
@@ -173,6 +176,32 @@ object TimelineMomentViewModel : ViewModel() {
_momentsFlow.value = updatedPagingData _momentsFlow.value = updatedPagingData
} }
fun updateFollowStatus(authorId:Int,isFollow:Boolean) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.authorId == authorId) {
momentItem.copy(followStatus = isFollow)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
fun followAction(moment: MomentEntity) {
viewModelScope.launch {
try {
if (moment.followStatus) {
userService.unFollowUser(moment.authorId.toString())
} else {
userService.followUser(moment.authorId.toString())
}
updateFollowStatus(moment.authorId, !moment.followStatus)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun ResetModel() { fun ResetModel() {
_momentsFlow.value = PagingData.empty() _momentsFlow.value = PagingData.empty()
isFirstLoad = true isFirstLoad = true

View File

@@ -265,7 +265,13 @@ fun MomentResultTab() {
.fillMaxWidth() .fillMaxWidth()
.background(Color.White) .background(Color.White)
) { ) {
MomentCard(momentEntity = momentItem, hideAction = true) MomentCard(
momentEntity = momentItem,
hideAction = true,
onFollowClick = {
model.momentFollowAction(momentItem)
}
)
} }
// Spacer(modifier = Modifier.padding(16.dp)) // Spacer(modifier = Modifier.padding(16.dp))
} }

View File

@@ -10,15 +10,14 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
import androidx.paging.map import androidx.paging.map
import com.aiosman.riderpro.data.MomentService
import com.aiosman.riderpro.data.UserServiceImpl
import com.aiosman.riderpro.entity.AccountPagingSource import com.aiosman.riderpro.entity.AccountPagingSource
import com.aiosman.riderpro.entity.AccountProfileEntity import com.aiosman.riderpro.entity.AccountProfileEntity
import com.aiosman.riderpro.entity.MomentEntity
import com.aiosman.riderpro.entity.MomentPagingSource import com.aiosman.riderpro.entity.MomentPagingSource
import com.aiosman.riderpro.entity.MomentRemoteDataSource import com.aiosman.riderpro.entity.MomentRemoteDataSource
import com.aiosman.riderpro.data.MomentService
import com.aiosman.riderpro.entity.MomentServiceImpl import com.aiosman.riderpro.entity.MomentServiceImpl
import com.aiosman.riderpro.data.UserServiceImpl
import com.aiosman.riderpro.entity.MomentEntity
import com.aiosman.riderpro.ui.index.tabs.moment.MomentViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@@ -99,4 +98,30 @@ object SearchViewModel : ViewModel() {
_usersFlow.value = PagingData.empty() _usersFlow.value = PagingData.empty()
showResult = false showResult = false
} }
fun updateMomentFollowStatus(authorId:Int,isFollow:Boolean) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.authorId == authorId) {
momentItem.copy(followStatus = isFollow)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
fun momentFollowAction(moment: MomentEntity) {
viewModelScope.launch {
try {
if (moment.followStatus) {
userService.unFollowUser(moment.authorId.toString())
} else {
userService.followUser(moment.authorId.toString())
}
updateMomentFollowStatus(moment.authorId, !moment.followStatus)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
} }