更新动态加载逻辑
This commit is contained in:
@@ -116,6 +116,8 @@ dependencies {
|
|||||||
implementation("androidx.core:core-splashscreen:1.0.1") // 添加 SplashScreen 依赖
|
implementation("androidx.core:core-splashscreen:1.0.1") // 添加 SplashScreen 依赖
|
||||||
// 添加 lifecycle-runtime-ktx 依赖
|
// 添加 lifecycle-runtime-ktx 依赖
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
|
||||||
|
implementation ("org.greenrobot:eventbus:3.3.1")
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import com.aiosman.ravenow.ui.follower.FollowerNoticeViewModel
|
|||||||
import com.aiosman.ravenow.ui.follower.FollowingListViewModel
|
import com.aiosman.ravenow.ui.follower.FollowingListViewModel
|
||||||
import com.aiosman.ravenow.ui.index.IndexViewModel
|
import com.aiosman.ravenow.ui.index.IndexViewModel
|
||||||
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel
|
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.MomentExploreViewModel
|
||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||||
import com.aiosman.ravenow.ui.index.tabs.search.DiscoverViewModel
|
import com.aiosman.ravenow.ui.index.tabs.search.DiscoverViewModel
|
||||||
@@ -131,6 +132,7 @@ object AppState {
|
|||||||
fun ReloadAppState(context: Context) {
|
fun ReloadAppState(context: Context) {
|
||||||
// 重置动态列表页面
|
// 重置动态列表页面
|
||||||
TimelineMomentViewModel.ResetModel()
|
TimelineMomentViewModel.ResetModel()
|
||||||
|
MomentExploreViewModel.ResetModel()
|
||||||
// 重置我的页面
|
// 重置我的页面
|
||||||
MyProfileViewModel.ResetModel()
|
MyProfileViewModel.ResetModel()
|
||||||
// 重置发现页面
|
// 重置发现页面
|
||||||
|
|||||||
60
app/src/main/java/com/aiosman/ravenow/entity/Loader.kt
Normal file
60
app/src/main/java/com/aiosman/ravenow/entity/Loader.kt
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
|
|
||||||
|
abstract class DataLoader<T,ET> {
|
||||||
|
var list: MutableList<T> = mutableListOf()
|
||||||
|
var page by mutableStateOf(1)
|
||||||
|
var total by mutableStateOf(0)
|
||||||
|
var pageSize by mutableStateOf(10)
|
||||||
|
var hasNext by mutableStateOf(true)
|
||||||
|
var onListChanged: ((List<T>) -> Unit)? = null
|
||||||
|
private var firstLoad = true
|
||||||
|
|
||||||
|
|
||||||
|
abstract suspend fun fetchData(
|
||||||
|
page: Int, pageSize: Int, extra: ET
|
||||||
|
): ListContainer<T>
|
||||||
|
|
||||||
|
suspend fun loadData(
|
||||||
|
extra: ET
|
||||||
|
) {
|
||||||
|
if (!firstLoad) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
firstLoad = false
|
||||||
|
val result = fetchData(page, pageSize, extra)
|
||||||
|
list = result.list.toMutableList()
|
||||||
|
this.page = page
|
||||||
|
this.total = result.total
|
||||||
|
this.pageSize = pageSize
|
||||||
|
this.hasNext = result.list.size == pageSize
|
||||||
|
onListChanged?.invoke(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadMore(extra: ET) {
|
||||||
|
if (firstLoad) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!hasNext) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val result = fetchData(page + 1, pageSize, extra)
|
||||||
|
list.addAll(result.list)
|
||||||
|
page += 1
|
||||||
|
hasNext = result.list.size == pageSize
|
||||||
|
onListChanged?.invoke(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
list.clear()
|
||||||
|
page = 1
|
||||||
|
total = 0
|
||||||
|
pageSize = 10
|
||||||
|
hasNext = true
|
||||||
|
firstLoad = true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -285,3 +285,68 @@ data class MomentEntity(
|
|||||||
// 是否收藏
|
// 是否收藏
|
||||||
var isFavorite: Boolean = false
|
var isFavorite: Boolean = false
|
||||||
)
|
)
|
||||||
|
class MomentLoaderExtraArgs(
|
||||||
|
val explore: Boolean? = false,
|
||||||
|
val timelineId: Int? = null,
|
||||||
|
val authorId : Int? = null
|
||||||
|
)
|
||||||
|
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
||||||
|
override suspend fun fetchData(
|
||||||
|
page: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
extra: MomentLoaderExtraArgs
|
||||||
|
): ListContainer<MomentEntity> {
|
||||||
|
val result = ApiClient.api.getPosts(
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
explore = if (extra.explore == true) "true" else "",
|
||||||
|
timelineId = extra.timelineId,
|
||||||
|
authorId = extra.authorId
|
||||||
|
)
|
||||||
|
val data = result.body()?.let {
|
||||||
|
ListContainer(
|
||||||
|
list = it.list.map { it.toMomentItem() },
|
||||||
|
total = it.total,
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (data == null) {
|
||||||
|
throw ServiceException("Failed to get moments")
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMomentLike(id: Int,isLike:Boolean) {
|
||||||
|
this.list = this.list.map { momentItem ->
|
||||||
|
if (momentItem.id == id) {
|
||||||
|
momentItem.copy(likeCount = momentItem.likeCount + if (isLike) 1 else -1, liked = isLike)
|
||||||
|
} else {
|
||||||
|
momentItem
|
||||||
|
}
|
||||||
|
}.toMutableList()
|
||||||
|
onListChanged?.invoke(this.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateFavoriteCount(id: Int,isFavorite:Boolean) {
|
||||||
|
this.list = this.list.map { momentItem ->
|
||||||
|
if (momentItem.id == id) {
|
||||||
|
momentItem.copy(favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1, isFavorite = isFavorite)
|
||||||
|
} else {
|
||||||
|
momentItem
|
||||||
|
}
|
||||||
|
}.toMutableList()
|
||||||
|
onListChanged?.invoke(this.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeMoment(id: Int) {
|
||||||
|
this.list = this.list.filter { it.id != id }.toMutableList()
|
||||||
|
onListChanged?.invoke(this.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addMoment(moment: MomentEntity) {
|
||||||
|
this.list.add(0, moment)
|
||||||
|
onListChanged?.invoke(this.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.aiosman.ravenow.event
|
||||||
|
|
||||||
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
|
|
||||||
|
data class MomentAddEvent(
|
||||||
|
val moment:MomentEntity
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.aiosman.ravenow.event
|
||||||
|
|
||||||
|
data class MomentFavouriteChangeEvent(
|
||||||
|
val postId: Int,
|
||||||
|
val isFavourite: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.aiosman.ravenow.event
|
||||||
|
|
||||||
|
data class MomentLikeChangeEvent(
|
||||||
|
val postId: Int,
|
||||||
|
val likeCount: Int?,
|
||||||
|
val isLike: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.aiosman.ravenow.event
|
||||||
|
|
||||||
|
data class MomentRemoveEvent(
|
||||||
|
val postId: Int
|
||||||
|
)
|
||||||
@@ -66,6 +66,7 @@ import com.aiosman.ravenow.ui.navigateToPost
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MomentCard(
|
fun MomentCard(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
momentEntity: MomentEntity,
|
momentEntity: MomentEntity,
|
||||||
onLikeClick: () -> Unit = {},
|
onLikeClick: () -> Unit = {},
|
||||||
onFavoriteClick: () -> Unit = {},
|
onFavoriteClick: () -> Unit = {},
|
||||||
@@ -78,7 +79,7 @@ fun MomentCard(
|
|||||||
var imageIndex by remember { mutableStateOf(0) }
|
var imageIndex by remember { mutableStateOf(0) }
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(AppColors.background)
|
.background(AppColors.background)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.aiosman.ravenow.data.MomentService
|
||||||
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
|
import com.aiosman.ravenow.entity.MomentLoader
|
||||||
|
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
|
||||||
|
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||||
|
import com.aiosman.ravenow.event.MomentAddEvent
|
||||||
|
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
|
||||||
|
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
||||||
|
import com.aiosman.ravenow.event.MomentRemoveEvent
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import org.greenrobot.eventbus.Subscribe
|
||||||
|
|
||||||
|
open class BaseMomentModel :ViewModel(){
|
||||||
|
private val momentService: MomentService = MomentServiceImpl()
|
||||||
|
private val userService = UserServiceImpl()
|
||||||
|
|
||||||
|
|
||||||
|
val momentLoader = MomentLoader().apply {
|
||||||
|
onListChanged = {
|
||||||
|
moments = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var refreshing by mutableStateOf(false)
|
||||||
|
var isFirstLoad = true
|
||||||
|
var moments by mutableStateOf<List<MomentEntity>>(listOf())
|
||||||
|
open fun extraArgs(): MomentLoaderExtraArgs {
|
||||||
|
return MomentLoaderExtraArgs()
|
||||||
|
}
|
||||||
|
fun refreshPager(pullRefresh: Boolean = false) {
|
||||||
|
if (!isFirstLoad && !pullRefresh) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isFirstLoad = false
|
||||||
|
momentLoader.clear()
|
||||||
|
viewModelScope.launch {
|
||||||
|
momentLoader.loadData(extraArgs())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMore() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
momentLoader.loadMore(extraArgs())
|
||||||
|
moments = momentLoader.list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun onMomentLikeChangeEvent(event: MomentLikeChangeEvent) {
|
||||||
|
momentLoader.updateMomentLike(event.postId, event.isLike)
|
||||||
|
}
|
||||||
|
suspend fun likeMoment(id: Int) {
|
||||||
|
momentService.likeMoment(id)
|
||||||
|
momentLoader.updateMomentLike(id, true)
|
||||||
|
}
|
||||||
|
suspend fun dislikeMoment(id: Int) {
|
||||||
|
momentService.dislikeMoment(id)
|
||||||
|
momentLoader.updateMomentLike(id, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun onAddComment(id: Int) {
|
||||||
|
// val currentPagingData = _momentsFlow.value
|
||||||
|
// updateCommentCount(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun onMomentFavoriteChangeEvent(event: MomentFavouriteChangeEvent) {
|
||||||
|
momentLoader.updateFavoriteCount(event.postId, event.isFavourite)
|
||||||
|
}
|
||||||
|
suspend fun favoriteMoment(id: Int) {
|
||||||
|
momentService.favoriteMoment(id)
|
||||||
|
momentLoader.updateFavoriteCount(id, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun unfavoriteMoment(id: Int) {
|
||||||
|
momentService.unfavoriteMoment(id)
|
||||||
|
momentLoader.updateFavoriteCount(id, false)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun onRemoveMomentEvent(event: MomentRemoveEvent) {
|
||||||
|
momentLoader.removeMoment(event.postId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun onAddMomentEvent(event: MomentAddEvent) {
|
||||||
|
momentLoader.addMoment(event.moment)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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() {
|
||||||
|
momentLoader.clear()
|
||||||
|
isFirstLoad = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
EventBus.getDefault().unregister(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,16 +4,19 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
|
||||||
import com.aiosman.ravenow.ui.composables.MomentCard
|
import com.aiosman.ravenow.ui.composables.MomentCard
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -24,14 +27,30 @@ import kotlinx.coroutines.launch
|
|||||||
@Composable
|
@Composable
|
||||||
fun ExploreMomentsList() {
|
fun ExploreMomentsList() {
|
||||||
val model = MomentExploreViewModel
|
val model = MomentExploreViewModel
|
||||||
var dataFlow = model.momentsFlow
|
var moments = model.moments
|
||||||
var moments = dataFlow.collectAsLazyPagingItems()
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val state = rememberPullRefreshState(model.refreshing, onRefresh = {
|
val state = rememberPullRefreshState(model.refreshing, onRefresh = {
|
||||||
model.refreshPager(
|
model.refreshPager(
|
||||||
pullRefresh = true
|
pullRefresh = true
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
// observe list scrolling
|
||||||
|
val reachedBottom by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()
|
||||||
|
lastVisibleItem?.index != 0 && lastVisibleItem?.index == listState.layoutInfo.totalItemsCount - 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load more if scrolled to bottom
|
||||||
|
LaunchedEffect(reachedBottom) {
|
||||||
|
if (reachedBottom) {
|
||||||
|
model.loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
model.refreshPager()
|
model.refreshPager()
|
||||||
}
|
}
|
||||||
@@ -43,9 +62,10 @@ fun ExploreMomentsList() {
|
|||||||
Box(Modifier.pullRefresh(state)) {
|
Box(Modifier.pullRefresh(state)) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = listState
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
moments.itemCount,
|
moments.size,
|
||||||
key = { idx -> idx }
|
key = { idx -> idx }
|
||||||
) { idx ->
|
) { idx ->
|
||||||
val momentItem = moments[idx] ?: return@items
|
val momentItem = moments[idx] ?: return@items
|
||||||
@@ -75,7 +95,7 @@ fun ExploreMomentsList() {
|
|||||||
},
|
},
|
||||||
onFollowClick = {
|
onFollowClick = {
|
||||||
model.followAction(momentItem)
|
model.followAction(momentItem)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre
|
||||||
|
|
||||||
|
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
|
||||||
|
|
||||||
|
object MomentExploreViewModel : BaseMomentModel() {
|
||||||
|
init {
|
||||||
|
EventBus.getDefault().register(this)
|
||||||
|
|
||||||
|
}
|
||||||
|
override fun extraArgs(): MomentLoaderExtraArgs {
|
||||||
|
return MomentLoaderExtraArgs(explore = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre
|
|
||||||
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.paging.Pager
|
|
||||||
import androidx.paging.PagingConfig
|
|
||||||
import androidx.paging.PagingData
|
|
||||||
import androidx.paging.cachedIn
|
|
||||||
import androidx.paging.map
|
|
||||||
import com.aiosman.ravenow.AppState
|
|
||||||
import com.aiosman.ravenow.data.MomentService
|
|
||||||
import com.aiosman.ravenow.data.UserServiceImpl
|
|
||||||
import com.aiosman.ravenow.entity.MomentEntity
|
|
||||||
import com.aiosman.ravenow.entity.MomentPagingSource
|
|
||||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
|
||||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
|
|
||||||
object MomentExploreViewModel : ViewModel() {
|
|
||||||
private val momentService: MomentService = MomentServiceImpl()
|
|
||||||
private val userService = UserServiceImpl()
|
|
||||||
private val _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
|
|
||||||
val momentsFlow = _momentsFlow.asStateFlow()
|
|
||||||
var existsMoment = mutableStateOf(false)
|
|
||||||
var refreshing by mutableStateOf(false)
|
|
||||||
var isFirstLoad = true
|
|
||||||
fun refreshPager(pullRefresh: Boolean = false) {
|
|
||||||
if (!isFirstLoad && !pullRefresh) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isFirstLoad = false
|
|
||||||
viewModelScope.launch {
|
|
||||||
if (pullRefresh) {
|
|
||||||
refreshing = true
|
|
||||||
}
|
|
||||||
// 检查是否有动态
|
|
||||||
val existMoments =
|
|
||||||
momentService.getMoments(timelineId = AppState.UserId, pageNumber = 1)
|
|
||||||
if (existMoments.list.isEmpty()) {
|
|
||||||
existsMoment.value = true
|
|
||||||
}
|
|
||||||
if (pullRefresh) {
|
|
||||||
refreshing = false
|
|
||||||
}
|
|
||||||
Pager(
|
|
||||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
|
||||||
pagingSourceFactory = {
|
|
||||||
MomentPagingSource(
|
|
||||||
MomentRemoteDataSource(momentService),
|
|
||||||
// 如果没有动态,则显示热门动态
|
|
||||||
explore = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
).flow.cachedIn(viewModelScope).collectLatest {
|
|
||||||
_momentsFlow.value = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateLikeCount(id: Int) {
|
|
||||||
val currentPagingData = _momentsFlow.value
|
|
||||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
|
||||||
if (momentItem.id == id) {
|
|
||||||
momentItem.copy(likeCount = momentItem.likeCount + 1, liked = true)
|
|
||||||
} else {
|
|
||||||
momentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_momentsFlow.value = updatedPagingData
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun likeMoment(id: Int) {
|
|
||||||
momentService.likeMoment(id)
|
|
||||||
updateLikeCount(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateCommentCount(id: Int) {
|
|
||||||
val currentPagingData = _momentsFlow.value
|
|
||||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
|
||||||
if (momentItem.id == id) {
|
|
||||||
momentItem.copy(commentCount = momentItem.commentCount + 1)
|
|
||||||
} else {
|
|
||||||
momentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_momentsFlow.value = updatedPagingData
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onAddComment(id: Int) {
|
|
||||||
val currentPagingData = _momentsFlow.value
|
|
||||||
updateCommentCount(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun updateDislikeMomentById(id: Int) {
|
|
||||||
val currentPagingData = _momentsFlow.value
|
|
||||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
|
||||||
if (momentItem.id == id) {
|
|
||||||
momentItem.copy(likeCount = momentItem.likeCount - 1, liked = false)
|
|
||||||
} else {
|
|
||||||
momentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_momentsFlow.value = updatedPagingData
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun dislikeMoment(id: Int) {
|
|
||||||
momentService.dislikeMoment(id)
|
|
||||||
updateDislikeMomentById(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateFavoriteCount(id: Int) {
|
|
||||||
val currentPagingData = _momentsFlow.value
|
|
||||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
|
||||||
if (momentItem.id == id) {
|
|
||||||
momentItem.copy(favoriteCount = momentItem.favoriteCount + 1, isFavorite = true)
|
|
||||||
} else {
|
|
||||||
momentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_momentsFlow.value = updatedPagingData
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun favoriteMoment(id: Int) {
|
|
||||||
momentService.favoriteMoment(id)
|
|
||||||
updateFavoriteCount(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateUnfavoriteCount(id: Int) {
|
|
||||||
val currentPagingData = _momentsFlow.value
|
|
||||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
|
||||||
if (momentItem.id == id) {
|
|
||||||
momentItem.copy(favoriteCount = momentItem.favoriteCount - 1, isFavorite = false)
|
|
||||||
} else {
|
|
||||||
momentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_momentsFlow.value = updatedPagingData
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun unfavoriteMoment(id: Int) {
|
|
||||||
momentService.unfavoriteMoment(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() {
|
|
||||||
_momentsFlow.value = PagingData.empty()
|
|
||||||
isFirstLoad = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,16 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -24,8 +28,7 @@ import kotlinx.coroutines.launch
|
|||||||
@Composable
|
@Composable
|
||||||
fun TimelineMomentsList() {
|
fun TimelineMomentsList() {
|
||||||
val model = TimelineMomentViewModel
|
val model = TimelineMomentViewModel
|
||||||
var dataFlow = model.momentsFlow
|
var moments = model.moments
|
||||||
var moments = dataFlow.collectAsLazyPagingItems()
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val state = rememberPullRefreshState(model.refreshing, onRefresh = {
|
val state = rememberPullRefreshState(model.refreshing, onRefresh = {
|
||||||
model.refreshPager(
|
model.refreshPager(
|
||||||
@@ -35,6 +38,20 @@ fun TimelineMomentsList() {
|
|||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
model.refreshPager()
|
model.refreshPager()
|
||||||
}
|
}
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
// observe list scrolling
|
||||||
|
val reachedBottom by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()
|
||||||
|
lastVisibleItem?.index != 0 && lastVisibleItem?.index == listState.layoutInfo.totalItemsCount - 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(reachedBottom) {
|
||||||
|
if (reachedBottom) {
|
||||||
|
model.loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -42,12 +59,13 @@ fun TimelineMomentsList() {
|
|||||||
Box(Modifier.pullRefresh(state)) {
|
Box(Modifier.pullRefresh(state)) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = listState
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
moments.itemCount,
|
moments.size,
|
||||||
key = { idx -> moments[idx]?.id ?: idx }
|
key = { idx -> moments[idx].id }
|
||||||
) { idx ->
|
) { idx ->
|
||||||
val momentItem = moments[idx] ?: return@items
|
val momentItem = moments[idx]
|
||||||
MomentCard(momentEntity = momentItem,
|
MomentCard(momentEntity = momentItem,
|
||||||
onAddComment = {
|
onAddComment = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -77,12 +95,6 @@ fun TimelineMomentsList() {
|
|||||||
},
|
},
|
||||||
showFollowButton = false
|
showFollowButton = false
|
||||||
)
|
)
|
||||||
// Box(
|
|
||||||
// modifier = Modifier
|
|
||||||
// .height(4.dp)
|
|
||||||
// .fillMaxWidth()
|
|
||||||
// .background(Color(0xFFF0F2F5))
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PullRefreshIndicator(model.refreshing, state, Modifier.align(Alignment.TopCenter))
|
PullRefreshIndicator(model.refreshing, state, Modifier.align(Alignment.TopCenter))
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline
|
|
||||||
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.paging.Pager
|
|
||||||
import androidx.paging.PagingConfig
|
|
||||||
import androidx.paging.PagingData
|
|
||||||
import androidx.paging.cachedIn
|
|
||||||
import androidx.paging.filter
|
|
||||||
import androidx.paging.map
|
|
||||||
import com.aiosman.ravenow.AppState
|
|
||||||
import com.aiosman.ravenow.data.MomentService
|
|
||||||
import com.aiosman.ravenow.data.UserService
|
|
||||||
import com.aiosman.ravenow.data.UserServiceImpl
|
|
||||||
import com.aiosman.ravenow.entity.MomentEntity
|
|
||||||
import com.aiosman.ravenow.entity.MomentPagingSource
|
|
||||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
|
||||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
|
|
||||||
object TimelineMomentViewModel : ViewModel() {
|
|
||||||
private val momentService: MomentService = MomentServiceImpl()
|
|
||||||
private val _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
|
|
||||||
private val userService :UserService = UserServiceImpl()
|
|
||||||
val momentsFlow = _momentsFlow.asStateFlow()
|
|
||||||
var existsMoment = mutableStateOf(false)
|
|
||||||
var refreshing by mutableStateOf(false)
|
|
||||||
var isFirstLoad = true
|
|
||||||
fun refreshPager(pullRefresh: Boolean = false) {
|
|
||||||
if (!isFirstLoad && !pullRefresh) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isFirstLoad = false
|
|
||||||
viewModelScope.launch {
|
|
||||||
if (pullRefresh) {
|
|
||||||
refreshing = true
|
|
||||||
}
|
|
||||||
// 检查是否有动态
|
|
||||||
val existMoments =
|
|
||||||
momentService.getMoments(timelineId = AppState.UserId, pageNumber = 1)
|
|
||||||
if (existMoments.list.isEmpty()) {
|
|
||||||
existsMoment.value = true
|
|
||||||
}
|
|
||||||
if (pullRefresh) {
|
|
||||||
refreshing = false
|
|
||||||
}
|
|
||||||
Pager(
|
|
||||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
|
||||||
pagingSourceFactory = {
|
|
||||||
MomentPagingSource(
|
|
||||||
MomentRemoteDataSource(momentService),
|
|
||||||
// 如果没有动态,则显示热门动态
|
|
||||||
timelineId = if (existMoments.list.isEmpty()) null else AppState.UserId,
|
|
||||||
trend = if (existMoments.list.isEmpty()) true else null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
).flow.cachedIn(viewModelScope).collectLatest {
|
|
||||||
_momentsFlow.value = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateLikeCount(id: Int) {
|
|
||||||
val currentPagingData = _momentsFlow.value
|
|
||||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
|
||||||
if (momentItem.id == id) {
|
|
||||||
momentItem.copy(likeCount = momentItem.likeCount + 1, liked = true)
|
|
||||||
} else {
|
|
||||||
momentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_momentsFlow.value = updatedPagingData
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun likeMoment(id: Int) {
|
|
||||||
momentService.likeMoment(id)
|
|
||||||
updateLikeCount(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateCommentCount(id: Int) {
|
|
||||||
val currentPagingData = _momentsFlow.value
|
|
||||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
|
||||||
if (momentItem.id == id) {
|
|
||||||
momentItem.copy(commentCount = momentItem.commentCount + 1)
|
|
||||||
} else {
|
|
||||||
momentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_momentsFlow.value = updatedPagingData
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onAddComment(id: Int) {
|
|
||||||
val currentPagingData = _momentsFlow.value
|
|
||||||
updateCommentCount(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun updateDislikeMomentById(id: Int) {
|
|
||||||
val currentPagingData = _momentsFlow.value
|
|
||||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
|
||||||
if (momentItem.id == id) {
|
|
||||||
momentItem.copy(likeCount = momentItem.likeCount - 1, liked = false)
|
|
||||||
} else {
|
|
||||||
momentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_momentsFlow.value = updatedPagingData
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun dislikeMoment(id: Int) {
|
|
||||||
momentService.dislikeMoment(id)
|
|
||||||
updateDislikeMomentById(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateFavoriteCount(id: Int) {
|
|
||||||
val currentPagingData = _momentsFlow.value
|
|
||||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
|
||||||
if (momentItem.id == id) {
|
|
||||||
momentItem.copy(favoriteCount = momentItem.favoriteCount + 1, isFavorite = true)
|
|
||||||
} else {
|
|
||||||
momentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_momentsFlow.value = updatedPagingData
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun favoriteMoment(id: Int) {
|
|
||||||
momentService.favoriteMoment(id)
|
|
||||||
updateFavoriteCount(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateUnfavoriteCount(id: Int) {
|
|
||||||
val currentPagingData = _momentsFlow.value
|
|
||||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
|
||||||
if (momentItem.id == id) {
|
|
||||||
momentItem.copy(favoriteCount = momentItem.favoriteCount - 1, isFavorite = false)
|
|
||||||
} else {
|
|
||||||
momentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_momentsFlow.value = updatedPagingData
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun unfavoriteMoment(id: Int) {
|
|
||||||
momentService.unfavoriteMoment(id)
|
|
||||||
updateUnfavoriteCount(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteMoment(id: Int) {
|
|
||||||
val currentPagingData = _momentsFlow.value
|
|
||||||
val updatedPagingData = currentPagingData.filter { momentItem ->
|
|
||||||
momentItem.id != id
|
|
||||||
}
|
|
||||||
_momentsFlow.value = updatedPagingData
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新动态评论数
|
|
||||||
*/
|
|
||||||
fun updateMomentCommentCount(id: Int, delta: Int) {
|
|
||||||
val currentPagingData = _momentsFlow.value
|
|
||||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
|
||||||
if (momentItem.id == id) {
|
|
||||||
momentItem.copy(commentCount = momentItem.commentCount + delta)
|
|
||||||
} else {
|
|
||||||
momentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_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() {
|
|
||||||
_momentsFlow.value = PagingData.empty()
|
|
||||||
isFirstLoad = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline
|
||||||
|
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
|
||||||
|
|
||||||
|
object TimelineMomentViewModel : BaseMomentModel() {
|
||||||
|
init {
|
||||||
|
EventBus.getDefault().register(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun extraArgs(): MomentLoaderExtraArgs {
|
||||||
|
return MomentLoaderExtraArgs(timelineId = AppState.UserId!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,11 +8,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.Pager
|
|
||||||
import androidx.paging.PagingConfig
|
|
||||||
import androidx.paging.PagingData
|
|
||||||
import androidx.paging.cachedIn
|
|
||||||
import androidx.paging.filter
|
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.ravenow.AppStore
|
import com.aiosman.ravenow.AppStore
|
||||||
import com.aiosman.ravenow.Messaging
|
import com.aiosman.ravenow.Messaging
|
||||||
@@ -22,28 +17,40 @@ import com.aiosman.ravenow.data.MomentService
|
|||||||
import com.aiosman.ravenow.data.UploadImage
|
import com.aiosman.ravenow.data.UploadImage
|
||||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
import com.aiosman.ravenow.entity.MomentEntity
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
import com.aiosman.ravenow.entity.MomentPagingSource
|
import com.aiosman.ravenow.entity.MomentLoader
|
||||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
|
||||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import com.aiosman.ravenow.event.MomentAddEvent
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
||||||
|
import com.aiosman.ravenow.event.MomentRemoveEvent
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import org.greenrobot.eventbus.Subscribe
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object MyProfileViewModel : ViewModel() {
|
object MyProfileViewModel : ViewModel() {
|
||||||
val accountService: AccountService = AccountServiceImpl()
|
val accountService: AccountService = AccountServiceImpl()
|
||||||
val momentService: MomentService = MomentServiceImpl()
|
val momentService: MomentService = MomentServiceImpl()
|
||||||
var profile by mutableStateOf<AccountProfileEntity?>(null)
|
var profile by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
private var _sharedFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
|
var moments by mutableStateOf<List<MomentEntity>>(emptyList())
|
||||||
var sharedFlow = _sharedFlow.asStateFlow()
|
val momentLoader: MomentLoader = MomentLoader().apply {
|
||||||
|
onListChanged = {
|
||||||
|
moments = it
|
||||||
|
}
|
||||||
|
}
|
||||||
var refreshing by mutableStateOf(false)
|
var refreshing by mutableStateOf(false)
|
||||||
var firstLoad = true
|
var firstLoad = true
|
||||||
|
|
||||||
|
init {
|
||||||
|
EventBus.getDefault().register(this)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun loadUserProfile() {
|
suspend fun loadUserProfile() {
|
||||||
val profile = accountService.getMyAccountProfile()
|
val profile = accountService.getMyAccountProfile()
|
||||||
MyProfileViewModel.profile = profile
|
MyProfileViewModel.profile = profile
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadProfile(pullRefresh: Boolean = false) {
|
fun loadProfile(pullRefresh: Boolean = false) {
|
||||||
if (!firstLoad && !pullRefresh) return
|
if (!firstLoad && !pullRefresh) return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -55,18 +62,7 @@ object MyProfileViewModel : ViewModel() {
|
|||||||
refreshing = false
|
refreshing = false
|
||||||
profile?.let {
|
profile?.let {
|
||||||
try {
|
try {
|
||||||
// Collect shared flow
|
momentLoader.loadData(extra = MomentLoaderExtraArgs(authorId = it.id))
|
||||||
Pager(
|
|
||||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
|
||||||
pagingSourceFactory = {
|
|
||||||
MomentPagingSource(
|
|
||||||
MomentRemoteDataSource(momentService),
|
|
||||||
author = AppState.UserId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
).flow.cachedIn(viewModelScope).collectLatest {
|
|
||||||
_sharedFlow.value = it
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("MyProfileViewModel", "loadProfile: ", e)
|
Log.e("MyProfileViewModel", "loadProfile: ", e)
|
||||||
}
|
}
|
||||||
@@ -75,6 +71,12 @@ object MyProfileViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadMoreMoment() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
momentLoader.loadMore(extra = MomentLoaderExtraArgs(authorId = profile?.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun logout(context: Context) {
|
fun logout(context: Context) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
Messaging.unregisterDevice(context)
|
Messaging.unregisterDevice(context)
|
||||||
@@ -89,7 +91,7 @@ object MyProfileViewModel : ViewModel() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateUserProfileBanner(bannerImageUrl: Uri?,file:File, context: Context) {
|
fun updateUserProfileBanner(bannerImageUrl: Uri?, file: File, context: Context) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val newBanner = bannerImageUrl?.let {
|
val newBanner = bannerImageUrl?.let {
|
||||||
val cursor = context.contentResolver.query(it, null, null, null, null)
|
val cursor = context.contentResolver.query(it, null, null, null, null)
|
||||||
@@ -117,21 +119,69 @@ object MyProfileViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteMoment(id: Int) {
|
fun likeMoment(momentLMomentEntity: MomentEntity) {
|
||||||
val currentPagingData = _sharedFlow.value
|
viewModelScope.launch {
|
||||||
val updatedPagingData = currentPagingData.filter { momentItem ->
|
if (momentLMomentEntity.liked) {
|
||||||
momentItem.id != id
|
momentService.dislikeMoment(momentLMomentEntity.id)
|
||||||
|
EventBus.getDefault().post(
|
||||||
|
MomentLikeChangeEvent(
|
||||||
|
momentLMomentEntity.id,
|
||||||
|
likeCount = momentLMomentEntity.likeCount - 1,
|
||||||
|
isLike = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
momentService.likeMoment(momentLMomentEntity.id)
|
||||||
|
EventBus.getDefault().post(
|
||||||
|
MomentLikeChangeEvent(
|
||||||
|
momentLMomentEntity.id,
|
||||||
|
likeCount = momentLMomentEntity.likeCount + 1,
|
||||||
|
isLike = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_sharedFlow.value = updatedPagingData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val bio get() = profile?.bio ?: ""
|
val bio get() = profile?.bio ?: ""
|
||||||
val nickName get() = profile?.nickName ?: ""
|
val nickName get() = profile?.nickName ?: ""
|
||||||
val avatar get() = profile?.avatar
|
val avatar get() = profile?.avatar
|
||||||
|
|
||||||
fun ResetModel() {
|
fun ResetModel() {
|
||||||
profile = null
|
profile = null
|
||||||
_sharedFlow.value = PagingData.empty()
|
momentLoader.clear()
|
||||||
firstLoad = true
|
firstLoad = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
EventBus.getDefault().unregister(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun onMomentLikeChangeEvent(event: MomentLikeChangeEvent) {
|
||||||
|
momentLoader.updateMomentLike(event.postId, event.isLike)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun onMomentFavoriteChangeEvent(event: MomentFavouriteChangeEvent) {
|
||||||
|
momentLoader.updateFavoriteCount(event.postId, event.isFavourite)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun onRemoveMomentEvent(event: MomentRemoveEvent) {
|
||||||
|
momentLoader.removeMoment(event.postId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun onAddMomentEvent(event: MomentAddEvent) {
|
||||||
|
momentLoader.addMoment(event.moment)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun onMomentDelete(event: MomentRemoveEvent) {
|
||||||
|
momentLoader.removeMoment(event.postId)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -23,8 +23,10 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
@@ -38,6 +40,8 @@ import androidx.compose.material.pullrefresh.pullRefresh
|
|||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -58,8 +62,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
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 androidx.paging.PagingData
|
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.ravenow.ConstVars
|
import com.aiosman.ravenow.ConstVars
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
@@ -88,9 +90,6 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
|||||||
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -102,22 +101,23 @@ fun ProfileV3(
|
|||||||
onLogout: () -> Unit = {},
|
onLogout: () -> Unit = {},
|
||||||
onFollowClick: () -> Unit = {},
|
onFollowClick: () -> Unit = {},
|
||||||
onChatClick: () -> Unit = {},
|
onChatClick: () -> Unit = {},
|
||||||
sharedFlow: SharedFlow<PagingData<MomentEntity>> = MutableStateFlow<PagingData<MomentEntity>>(
|
moments: List<MomentEntity>,
|
||||||
PagingData.empty()
|
isSelf: Boolean = true,
|
||||||
).asStateFlow(),
|
onLoadMore: () -> Unit = {},
|
||||||
isSelf: Boolean = true
|
onLike: (MomentEntity) -> Unit = {},
|
||||||
|
onComment: (MomentEntity) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val model = MyProfileViewModel
|
val model = MyProfileViewModel
|
||||||
val state = rememberCollapsingToolbarScaffoldState()
|
val state = rememberCollapsingToolbarScaffoldState()
|
||||||
val pagerState = rememberPagerState(pageCount = { 2 })
|
val pagerState = rememberPagerState(pageCount = { 2 })
|
||||||
var enabled by remember { mutableStateOf(true) }
|
val enabled by remember { mutableStateOf(true) }
|
||||||
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
var minibarExpanded by remember { mutableStateOf(false) }
|
var minibarExpanded by remember { mutableStateOf(false) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
var bannerHeight = 400
|
val bannerHeight = 400
|
||||||
val pickBannerImageLauncher = pickupAndCompressLauncher(
|
val pickBannerImageLauncher = pickupAndCompressLauncher(
|
||||||
context,
|
context,
|
||||||
scope,
|
scope,
|
||||||
@@ -126,15 +126,44 @@ fun ProfileV3(
|
|||||||
) { uri, file ->
|
) { uri, file ->
|
||||||
onUpdateBanner?.invoke(uri, file, context)
|
onUpdateBanner?.invoke(uri, file, context)
|
||||||
}
|
}
|
||||||
val moments = sharedFlow.collectAsLazyPagingItems()
|
|
||||||
val refreshState = rememberPullRefreshState(model.refreshing, onRefresh = {
|
val refreshState = rememberPullRefreshState(model.refreshing, onRefresh = {
|
||||||
model.loadProfile(pullRefresh = true)
|
model.loadProfile(pullRefresh = true)
|
||||||
})
|
})
|
||||||
var miniToolbarHeight by remember { mutableStateOf(0) }
|
var miniToolbarHeight by remember { mutableStateOf(0) }
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
var appTheme = LocalAppTheme.current
|
val appTheme = LocalAppTheme.current
|
||||||
var AppColors = appTheme
|
val AppColors = appTheme
|
||||||
var systemUiController = rememberSystemUiController()
|
val systemUiController = rememberSystemUiController()
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
// observe list scrolling
|
||||||
|
val reachedListBottom by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()
|
||||||
|
lastVisibleItem?.index != 0 && lastVisibleItem?.index == listState.layoutInfo.totalItemsCount - 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load more if scrolled to bottom
|
||||||
|
LaunchedEffect(reachedListBottom) {
|
||||||
|
if (reachedListBottom) {
|
||||||
|
onLoadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val gridState = rememberLazyStaggeredGridState()
|
||||||
|
val reachedGridBottom by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
val lastVisibleItem = gridState.layoutInfo.visibleItemsInfo.lastOrNull()
|
||||||
|
lastVisibleItem?.index != 0 && lastVisibleItem?.index == gridState.layoutInfo.totalItemsCount - 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(reachedGridBottom) {
|
||||||
|
if (reachedGridBottom) {
|
||||||
|
onLoadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun switchTheme(){
|
fun switchTheme(){
|
||||||
// delay
|
// delay
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -499,7 +528,8 @@ fun ProfileV3(
|
|||||||
8.dp
|
8.dp
|
||||||
),
|
),
|
||||||
verticalItemSpacing = 8.dp,
|
verticalItemSpacing = 8.dp,
|
||||||
contentPadding = PaddingValues(8.dp)
|
contentPadding = PaddingValues(8.dp),
|
||||||
|
state = gridState
|
||||||
) {
|
) {
|
||||||
if (isSelf) {
|
if (isSelf) {
|
||||||
items(1) {
|
items(1) {
|
||||||
@@ -541,7 +571,7 @@ fun ProfileV3(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items(moments.itemCount) { idx ->
|
items(moments.size) { idx ->
|
||||||
val moment = moments[idx] ?: return@items
|
val moment = moments[idx] ?: return@items
|
||||||
GalleryItem(moment, idx)
|
GalleryItem(moment, idx)
|
||||||
}
|
}
|
||||||
@@ -553,18 +583,26 @@ fun ProfileV3(
|
|||||||
1 ->
|
1 ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize(),
|
||||||
|
state = listState
|
||||||
) {
|
) {
|
||||||
if (moments.itemCount == 0 && isSelf) {
|
if (moments.isEmpty() && isSelf) {
|
||||||
item {
|
item {
|
||||||
EmptyMomentPostUnit()
|
EmptyMomentPostUnit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
for (idx in 0 until moments.itemCount) {
|
for (element in moments) {
|
||||||
val moment = moments[idx]
|
element.let {
|
||||||
moment?.let {
|
MomentPostUnit(
|
||||||
MomentPostUnit(it)
|
it,
|
||||||
|
onLikeClick = {
|
||||||
|
onLike(it)
|
||||||
|
},
|
||||||
|
onCommentClick = {
|
||||||
|
onComment(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ package com.aiosman.ravenow.ui.index.tabs.profile
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.ui.navigateToPost
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileWrap(
|
fun ProfileWrap(
|
||||||
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
MyProfileViewModel.loadProfile()
|
MyProfileViewModel.loadProfile()
|
||||||
}
|
}
|
||||||
@@ -20,6 +23,15 @@ fun ProfileWrap(
|
|||||||
MyProfileViewModel.logout(context)
|
MyProfileViewModel.logout(context)
|
||||||
},
|
},
|
||||||
profile = MyProfileViewModel.profile,
|
profile = MyProfileViewModel.profile,
|
||||||
sharedFlow = MyProfileViewModel.sharedFlow,
|
moments = MyProfileViewModel.moments,
|
||||||
|
onLoadMore = {
|
||||||
|
MyProfileViewModel.loadMoreMoment()
|
||||||
|
},
|
||||||
|
onLike = { moments ->
|
||||||
|
MyProfileViewModel.likeMoment(moments)
|
||||||
|
},
|
||||||
|
onComment = {
|
||||||
|
navController.navigateToPost(it.id)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,9 @@ import com.aiosman.ravenow.R
|
|||||||
import com.aiosman.ravenow.entity.MomentEntity
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
import com.aiosman.ravenow.exp.formatPostTime2
|
import com.aiosman.ravenow.exp.formatPostTime2
|
||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.composables.AnimatedLikeIcon
|
||||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.MomentOperateBtn
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.ravenow.ui.navigateToPost
|
import com.aiosman.ravenow.ui.navigateToPost
|
||||||
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
||||||
@@ -130,20 +132,26 @@ fun ProfileEmptyMomentCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MomentPostUnit(momentEntity: MomentEntity) {
|
fun MomentPostUnit(
|
||||||
|
momentEntity: MomentEntity,
|
||||||
|
onCommentClick: () -> Unit = {},
|
||||||
|
onLikeClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
TimeGroup(momentEntity.time.formatPostTime2())
|
TimeGroup(momentEntity.time.formatPostTime2())
|
||||||
ProfileMomentCard(
|
ProfileMomentCard(
|
||||||
momentEntity.momentTextContent,
|
momentEntity.momentTextContent,
|
||||||
momentEntity.images[0].thumbnail,
|
momentEntity.images[0].thumbnail,
|
||||||
momentEntity.likeCount.toString(),
|
momentEntity.likeCount.toString(),
|
||||||
momentEntity.commentCount.toString(),
|
momentEntity.commentCount.toString(),
|
||||||
momentEntity = momentEntity
|
momentEntity = momentEntity,
|
||||||
|
onCommentClick = onCommentClick,
|
||||||
|
onLikeClick = onLikeClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimeGroup(time: String = "2024.06.08 12:23") {
|
fun TimeGroup(time: String = "2024.06.08 12:23") {
|
||||||
val AppColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -162,7 +170,7 @@ fun TimeGroup(time: String = "2024.06.08 12:23") {
|
|||||||
Text(
|
Text(
|
||||||
text = time,
|
text = time,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = AppColors.text,
|
color = appColors.text,
|
||||||
style = TextStyle(fontWeight = FontWeight.W600)
|
style = TextStyle(fontWeight = FontWeight.W600)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -174,7 +182,9 @@ fun ProfileMomentCard(
|
|||||||
imageUrl: String,
|
imageUrl: String,
|
||||||
like: String,
|
like: String,
|
||||||
comment: String,
|
comment: String,
|
||||||
momentEntity: MomentEntity
|
momentEntity: MomentEntity,
|
||||||
|
onCommentClick: () -> Unit = {},
|
||||||
|
onLikeClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
var columnHeight by remember { mutableStateOf(0) }
|
var columnHeight by remember { mutableStateOf(0) }
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
@@ -214,7 +224,13 @@ fun ProfileMomentCard(
|
|||||||
MomentCardTopContent(content)
|
MomentCardTopContent(content)
|
||||||
}
|
}
|
||||||
MomentCardPicture(imageUrl, momentEntity = momentEntity)
|
MomentCardPicture(imageUrl, momentEntity = momentEntity)
|
||||||
MomentCardOperation(like, comment)
|
MomentCardOperation(
|
||||||
|
like = like,
|
||||||
|
isLike = momentEntity.liked,
|
||||||
|
onLikeClick = onLikeClick,
|
||||||
|
comment = comment,
|
||||||
|
onCommentClick = onCommentClick
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,7 +278,13 @@ fun MomentCardPicture(imageUrl: String, momentEntity: MomentEntity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MomentCardOperation(like: String, comment: String) {
|
fun MomentCardOperation(
|
||||||
|
like: String,
|
||||||
|
isLike: Boolean,
|
||||||
|
onLikeClick: () -> Unit,
|
||||||
|
comment: String,
|
||||||
|
onCommentClick: () -> Unit
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -270,16 +292,24 @@ fun MomentCardOperation(like: String, comment: String) {
|
|||||||
horizontalArrangement = Arrangement.End,
|
horizontalArrangement = Arrangement.End,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
MomentCardOperationItem(
|
MomentOperateBtn(count = like) {
|
||||||
drawable = R.drawable.rider_pro_like,
|
AnimatedLikeIcon(
|
||||||
number = like,
|
liked = isLike,
|
||||||
modifier = Modifier.padding(end = 32.dp)
|
onClick = onLikeClick,
|
||||||
)
|
)
|
||||||
MomentCardOperationItem(
|
}
|
||||||
drawable = R.drawable.rider_pro_moment_comment,
|
|
||||||
number = comment,
|
MomentOperateBtn(count = comment) {
|
||||||
modifier = Modifier.padding(end = 32.dp)
|
Image(
|
||||||
)
|
painter = painterResource(id = R.drawable.rider_pro_moment_comment),
|
||||||
|
contentDescription = "",
|
||||||
|
colorFilter = ColorFilter.tint(LocalAppTheme.current.text),
|
||||||
|
modifier = Modifier.noRippleClickable {
|
||||||
|
onCommentClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ class CommentsViewModel(
|
|||||||
replyUserId = replyUserId,
|
replyUserId = replyUserId,
|
||||||
replyCommentId = replyCommentId
|
replyCommentId = replyCommentId
|
||||||
)
|
)
|
||||||
TimelineMomentViewModel.updateCommentCount(postId.toInt())
|
// TimelineMomentViewModel.updateCommentCount(postId.toInt())
|
||||||
// add to first
|
// add to first
|
||||||
addedCommentList = listOf(comment) + addedCommentList
|
addedCommentList = listOf(comment) + addedCommentList
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.aiosman.ravenow.data.MomentService
|
|||||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||||
import com.aiosman.ravenow.data.UploadImage
|
import com.aiosman.ravenow.data.UploadImage
|
||||||
import com.aiosman.ravenow.entity.MomentEntity
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
|
import com.aiosman.ravenow.event.MomentAddEvent
|
||||||
import com.aiosman.ravenow.exp.rotate
|
import com.aiosman.ravenow.exp.rotate
|
||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||||
@@ -21,6 +22,7 @@ import com.aiosman.ravenow.ui.modification.Modification
|
|||||||
import com.aiosman.ravenow.utils.FileUtil
|
import com.aiosman.ravenow.utils.FileUtil
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -141,10 +143,10 @@ object NewPostViewModel : ViewModel() {
|
|||||||
onUploadProgress(((index / imageList.size).toFloat())) // progressValue 是当前上传进度,例如 0.5 表示 50%
|
onUploadProgress(((index / imageList.size).toFloat())) // progressValue 是当前上传进度,例如 0.5 表示 50%
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
momentService.createMoment(textContent, 1, uploadImageList, relPostId)
|
val result = momentService.createMoment(textContent, 1, uploadImageList, relPostId)
|
||||||
// 刷新个人动态
|
// 刷新个人动态
|
||||||
MyProfileViewModel.loadProfile(pullRefresh = true)
|
MyProfileViewModel.loadProfile(pullRefresh = true)
|
||||||
TimelineMomentViewModel.refreshPager()
|
EventBus.getDefault().post(MomentAddEvent(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun init() {
|
suspend fun init() {
|
||||||
|
|||||||
@@ -13,9 +13,14 @@ import com.aiosman.ravenow.data.UserServiceImpl
|
|||||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
import com.aiosman.ravenow.entity.MomentEntity
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||||
|
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
|
||||||
|
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
||||||
|
import com.aiosman.ravenow.event.MomentRemoveEvent
|
||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import org.greenrobot.eventbus.Subscribe
|
||||||
|
|
||||||
|
|
||||||
class PostViewModel(
|
class PostViewModel(
|
||||||
@@ -31,13 +36,16 @@ class PostViewModel(
|
|||||||
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId)
|
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId)
|
||||||
var isError by mutableStateOf(false)
|
var isError by mutableStateOf(false)
|
||||||
var isFirstLoad by mutableStateOf(true)
|
var isFirstLoad by mutableStateOf(true)
|
||||||
|
init {
|
||||||
|
EventBus.getDefault().register(this)
|
||||||
|
}
|
||||||
|
|
||||||
fun reloadComment() {
|
fun reloadComment() {
|
||||||
commentsViewModel.reloadComment()
|
commentsViewModel.reloadComment()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun initData(highlightCommentId: Int? = null) {
|
suspend fun initData(highlightCommentId: Int? = null) {
|
||||||
if (!isFirstLoad) {
|
if (!isFirstLoad) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isFirstLoad = false
|
isFirstLoad = false
|
||||||
@@ -77,37 +85,80 @@ class PostViewModel(
|
|||||||
moment = moment?.copy(commentCount = moment?.commentCount?.plus(1) ?: 0)
|
moment = moment?.copy(commentCount = moment?.commentCount?.plus(1) ?: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun onMomentLikeChangeEvent(event: MomentLikeChangeEvent) {
|
||||||
|
moment?.let {
|
||||||
|
if (event.postId == it.id) {
|
||||||
|
moment = it.copy(likeCount = event.likeCount ?: it.likeCount, liked = event.isLike)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun likeMoment() {
|
suspend fun likeMoment() {
|
||||||
moment?.let {
|
moment?.let {
|
||||||
service.likeMoment(it.id)
|
service.likeMoment(it.id)
|
||||||
moment = moment?.copy(likeCount = moment?.likeCount?.plus(1) ?: 0, liked = true)
|
EventBus.getDefault().post(
|
||||||
TimelineMomentViewModel.updateLikeCount(it.id)
|
MomentLikeChangeEvent(
|
||||||
|
postId = it.id,
|
||||||
|
likeCount = it.likeCount + 1,
|
||||||
|
isLike = true
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun dislikeMoment() {
|
suspend fun dislikeMoment() {
|
||||||
moment?.let {
|
moment?.let {
|
||||||
service.dislikeMoment(it.id)
|
service.dislikeMoment(it.id)
|
||||||
moment = moment?.copy(likeCount = moment?.likeCount?.minus(1) ?: 0, liked = false)
|
EventBus.getDefault().post(
|
||||||
// update home list
|
MomentLikeChangeEvent(
|
||||||
TimelineMomentViewModel.updateDislikeMomentById(it.id)
|
postId = it.id,
|
||||||
|
likeCount = it.likeCount - 1,
|
||||||
|
isLike = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun onMomentFavouriteChangeEvent(event: MomentFavouriteChangeEvent) {
|
||||||
|
moment?.let {
|
||||||
|
if (event.postId == it.id) {
|
||||||
|
val favouriteCount = if (event.isFavourite) {
|
||||||
|
it.favoriteCount + 1
|
||||||
|
} else {
|
||||||
|
it.favoriteCount - 1
|
||||||
|
}
|
||||||
|
moment = it.copy(
|
||||||
|
favoriteCount = favouriteCount,
|
||||||
|
isFavorite = event.isFavourite
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun favoriteMoment() {
|
suspend fun favoriteMoment() {
|
||||||
moment?.let {
|
moment?.let {
|
||||||
service.favoriteMoment(it.id)
|
service.favoriteMoment(it.id)
|
||||||
moment =
|
EventBus.getDefault().post(
|
||||||
moment?.copy(favoriteCount = moment?.favoriteCount?.plus(1) ?: 0, isFavorite = true)
|
MomentFavouriteChangeEvent(
|
||||||
|
postId = it.id,
|
||||||
|
isFavourite = true
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun unfavoriteMoment() {
|
suspend fun unfavoriteMoment() {
|
||||||
moment?.let {
|
moment?.let {
|
||||||
service.unfavoriteMoment(it.id)
|
service.unfavoriteMoment(it.id)
|
||||||
moment = moment?.copy(
|
EventBus.getDefault().post(
|
||||||
favoriteCount = moment?.favoriteCount?.minus(1) ?: 0, isFavorite = false
|
MomentFavouriteChangeEvent(
|
||||||
|
postId = it.id,
|
||||||
|
isFavourite = false
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +182,6 @@ class PostViewModel(
|
|||||||
commentsViewModel.deleteComment(commentId)
|
commentsViewModel.deleteComment(commentId)
|
||||||
moment = moment?.copy(commentCount = moment?.commentCount?.minus(1) ?: 0)
|
moment = moment?.copy(commentCount = moment?.commentCount?.minus(1) ?: 0)
|
||||||
moment?.let {
|
moment?.let {
|
||||||
TimelineMomentViewModel.updateMomentCommentCount(it.id, -1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,8 +210,7 @@ class PostViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
moment?.let {
|
moment?.let {
|
||||||
service.deleteMoment(it.id)
|
service.deleteMoment(it.id)
|
||||||
TimelineMomentViewModel.deleteMoment(it.id)
|
EventBus.getDefault().post(MomentRemoveEvent(it.id))
|
||||||
MyProfileViewModel.deleteMoment(it.id)
|
|
||||||
}
|
}
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
@@ -171,4 +220,8 @@ class PostViewModel(
|
|||||||
commentsViewModel.loadMoreSubComments(commentId)
|
commentsViewModel.loadMoreSubComments(commentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
EventBus.getDefault().unregister(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ fun AccountProfileV2(id: String){
|
|||||||
isSelf = true
|
isSelf = true
|
||||||
}
|
}
|
||||||
ProfileV3(
|
ProfileV3(
|
||||||
sharedFlow = model.momentsFlow,
|
moments = model.moments,
|
||||||
profile = model.profile,
|
profile = model.profile,
|
||||||
isSelf = isSelf,
|
isSelf = isSelf,
|
||||||
onChatClick = {
|
onChatClick = {
|
||||||
|
|||||||
@@ -5,34 +5,29 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.Pager
|
|
||||||
import androidx.paging.PagingConfig
|
|
||||||
import androidx.paging.PagingData
|
|
||||||
import androidx.paging.cachedIn
|
|
||||||
import com.aiosman.ravenow.data.AccountService
|
import com.aiosman.ravenow.data.AccountService
|
||||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
import com.aiosman.ravenow.data.MomentService
|
|
||||||
import com.aiosman.ravenow.data.UserServiceImpl
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
import com.aiosman.ravenow.entity.MomentEntity
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
import com.aiosman.ravenow.entity.MomentPagingSource
|
import com.aiosman.ravenow.entity.MomentLoader
|
||||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
|
||||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
class AccountProfileViewModel : ViewModel() {
|
class AccountProfileViewModel : ViewModel() {
|
||||||
var profileId by mutableStateOf(0)
|
var profileId by mutableStateOf(0)
|
||||||
val accountService: AccountService = AccountServiceImpl()
|
val accountService: AccountService = AccountServiceImpl()
|
||||||
val momentService: MomentService = MomentServiceImpl()
|
|
||||||
val userService = UserServiceImpl()
|
val userService = UserServiceImpl()
|
||||||
var profile by mutableStateOf<AccountProfileEntity?>(null)
|
var profile by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
private var _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
|
|
||||||
var momentsFlow = _momentsFlow.asStateFlow()
|
|
||||||
var refreshing by mutableStateOf(false)
|
var refreshing by mutableStateOf(false)
|
||||||
|
var momentLoader = MomentLoader().apply {
|
||||||
|
onListChanged = {
|
||||||
|
moments = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var moments by mutableStateOf<List<MomentEntity>>(listOf())
|
||||||
|
|
||||||
fun loadProfile(id: String, pullRefresh: Boolean = false) {
|
fun loadProfile(id: String, pullRefresh: Boolean = false) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (pullRefresh) {
|
if (pullRefresh) {
|
||||||
@@ -43,16 +38,12 @@ class AccountProfileViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
profile = userService.getUserProfile(id)
|
profile = userService.getUserProfile(id)
|
||||||
refreshing = false
|
refreshing = false
|
||||||
Pager(
|
profile?.let {
|
||||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
try {
|
||||||
pagingSourceFactory = {
|
momentLoader.loadData(MomentLoaderExtraArgs(authorId = it.id))
|
||||||
MomentPagingSource(
|
} catch (e: Exception) {
|
||||||
MomentRemoteDataSource(momentService),
|
e.printStackTrace()
|
||||||
author = profile!!.id
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
).flow.cachedIn(viewModelScope).collectLatest {
|
|
||||||
_momentsFlow.value = it
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,8 +63,6 @@ class AccountProfileViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val followerCount get() = profile?.followerCount ?: 0
|
|
||||||
val followingCount get() = profile?.followingCount ?: 0
|
|
||||||
val bio get() = profile?.bio ?: ""
|
val bio get() = profile?.bio ?: ""
|
||||||
val nickName get() = profile?.nickName ?: ""
|
val nickName get() = profile?.nickName ?: ""
|
||||||
val avatar get() = profile?.avatar
|
val avatar get() = profile?.avatar
|
||||||
|
|||||||
Reference in New Issue
Block a user