refactor(posts, communities): immediate feedback after actions

This commit is contained in:
Diego Beraldin 2023-08-10 15:35:20 +02:00
parent 9e39f240ee
commit c26ad24358
39 changed files with 781 additions and 611 deletions

View File

@ -1,23 +1,23 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.di
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreenViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreenViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailViewModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import org.koin.core.parameter.parametersOf
import org.koin.java.KoinJavaComponent.inject
actual fun getPostDetailScreenViewModel(post: PostModel): PostDetailScreenViewModel {
val res: PostDetailScreenViewModel by inject(
clazz = PostDetailScreenViewModel::class.java,
actual fun getPostDetailScreenViewModel(post: PostModel): PostDetailViewModel {
val res: PostDetailViewModel by inject(
clazz = PostDetailViewModel::class.java,
parameters = { parametersOf(post) },
)
return res
}
actual fun getCommunityDetailScreenViewModel(community: CommunityModel): CommunityDetailScreenViewModel {
val res: CommunityDetailScreenViewModel by inject(
clazz = CommunityDetailScreenViewModel::class.java,
actual fun getCommunityDetailScreenViewModel(community: CommunityModel): CommunityDetailViewModel {
val res: CommunityDetailViewModel by inject(
clazz = CommunityDetailViewModel::class.java,
parameters = { parametersOf(community) },
)
return res

View File

@ -4,8 +4,8 @@ import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
interface CommunityDetailScreenMviModel :
MviModel<CommunityDetailScreenMviModel.Intent, CommunityDetailScreenMviModel.UiState, CommunityDetailScreenMviModel.Effect> {
interface CommunityDetailMviModel :
MviModel<CommunityDetailMviModel.Intent, CommunityDetailMviModel.UiState, CommunityDetailMviModel.Effect> {
sealed interface Intent {
object Refresh : Intent

View File

@ -93,7 +93,7 @@ class CommunityDetailScreen(
) { padding ->
val community = uiState.community
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
model.reduce(CommunityDetailScreenMviModel.Intent.Refresh)
model.reduce(CommunityDetailMviModel.Intent.Refresh)
})
Box(
modifier = Modifier.pullRefresh(pullRefreshState),
@ -186,7 +186,7 @@ class CommunityDetailScreen(
post = post,
onUpVote = {
model.reduce(
CommunityDetailScreenMviModel.Intent.UpVotePost(
CommunityDetailMviModel.Intent.UpVotePost(
it,
post,
),
@ -194,7 +194,7 @@ class CommunityDetailScreen(
},
onDownVote = {
model.reduce(
CommunityDetailScreenMviModel.Intent.DownVotePost(
CommunityDetailMviModel.Intent.DownVotePost(
it,
post,
),
@ -202,7 +202,7 @@ class CommunityDetailScreen(
},
onSave = {
model.reduce(
CommunityDetailScreenMviModel.Intent.SavePost(
CommunityDetailMviModel.Intent.SavePost(
it,
post,
),
@ -212,7 +212,7 @@ class CommunityDetailScreen(
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
model.reduce(CommunityDetailScreenMviModel.Intent.LoadNextPage)
model.reduce(CommunityDetailMviModel.Intent.LoadNextPage)
}
if (uiState.loading && !uiState.refreshing) {
Box(

View File

@ -1,164 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.KeyStoreKeys
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.TemporaryKeyStore
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toSortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostsRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
class CommunityDetailScreenViewModel(
private val mvi: DefaultMviModel<CommunityDetailScreenMviModel.Intent, CommunityDetailScreenMviModel.UiState, CommunityDetailScreenMviModel.Effect>,
private val community: CommunityModel,
private val identityRepository: IdentityRepository,
private val postsRepository: PostsRepository,
private val keyStore: TemporaryKeyStore,
) : MviModel<CommunityDetailScreenMviModel.Intent, CommunityDetailScreenMviModel.UiState, CommunityDetailScreenMviModel.Effect> by mvi,
ScreenModel {
private var currentPage: Int = 1
override fun onStarted() {
mvi.onStarted()
mvi.updateState { it.copy(community = community) }
if (mvi.uiState.value.posts.isEmpty()) {
refresh()
}
}
override fun reduce(intent: CommunityDetailScreenMviModel.Intent) {
when (intent) {
CommunityDetailScreenMviModel.Intent.LoadNextPage -> loadNextPage()
CommunityDetailScreenMviModel.Intent.Refresh -> refresh()
is CommunityDetailScreenMviModel.Intent.DownVotePost -> downVotePost(
intent.post,
intent.value,
)
is CommunityDetailScreenMviModel.Intent.SavePost -> savePost(
intent.post,
intent.value,
)
is CommunityDetailScreenMviModel.Intent.UpVotePost -> upVotePost(
intent.post,
intent.value,
)
}
}
private fun refresh() {
currentPage = 1
mvi.updateState { it.copy(canFetchMore = true, refreshing = true) }
loadNextPage()
}
private fun loadNextPage() {
val currentState = mvi.uiState.value
if (!currentState.canFetchMore || currentState.loading) {
return
}
mvi.scope.launch(Dispatchers.IO) {
mvi.updateState { it.copy(loading = true) }
val auth = identityRepository.authToken.value
val refreshing = currentState.refreshing
val sort = keyStore[KeyStoreKeys.DefaultCommentSortType, 3].toSortType()
val commentList = postsRepository.getAll(
auth = auth,
communityId = community.id,
page = currentPage,
sort = sort,
)
currentPage++
val canFetchMore = commentList.size >= CommentRepository.DEFAULT_PAGE_SIZE
mvi.updateState {
val newItems = if (refreshing) {
commentList
} else {
it.posts + commentList
}
it.copy(
posts = newItems,
loading = false,
canFetchMore = canFetchMore,
refreshing = false,
)
}
}
}
private fun upVotePost(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newItem = postsRepository.upVote(
auth = auth,
post = post,
voted = value,
)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newItem
} else {
p
}
},
)
}
}
}
private fun downVotePost(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newItem = postsRepository.downVote(
auth = auth,
post = post,
downVoted = value,
)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newItem
} else {
p
}
},
)
}
}
}
private fun savePost(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newItem = postsRepository.save(
auth = auth,
post = post,
saved = value,
)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newItem
} else {
p
}
},
)
}
}
}
}

View File

@ -0,0 +1,212 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.KeyStoreKeys
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.TemporaryKeyStore
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toSortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostsRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
class CommunityDetailViewModel(
private val mvi: DefaultMviModel<CommunityDetailMviModel.Intent, CommunityDetailMviModel.UiState, CommunityDetailMviModel.Effect>,
private val community: CommunityModel,
private val identityRepository: IdentityRepository,
private val postsRepository: PostsRepository,
private val keyStore: TemporaryKeyStore,
) : MviModel<CommunityDetailMviModel.Intent, CommunityDetailMviModel.UiState, CommunityDetailMviModel.Effect> by mvi,
ScreenModel {
private var currentPage: Int = 1
override fun onStarted() {
mvi.onStarted()
mvi.updateState { it.copy(community = community) }
if (mvi.uiState.value.posts.isEmpty()) {
refresh()
}
}
override fun reduce(intent: CommunityDetailMviModel.Intent) {
when (intent) {
CommunityDetailMviModel.Intent.LoadNextPage -> loadNextPage()
CommunityDetailMviModel.Intent.Refresh -> refresh()
is CommunityDetailMviModel.Intent.DownVotePost -> downVotePost(
intent.post,
intent.value,
)
is CommunityDetailMviModel.Intent.SavePost -> savePost(
intent.post,
intent.value,
)
is CommunityDetailMviModel.Intent.UpVotePost -> upVotePost(
intent.post,
intent.value,
)
}
}
private fun refresh() {
currentPage = 1
mvi.updateState { it.copy(canFetchMore = true, refreshing = true) }
loadNextPage()
}
private fun loadNextPage() {
val currentState = mvi.uiState.value
if (!currentState.canFetchMore || currentState.loading) {
return
}
mvi.scope.launch(Dispatchers.IO) {
mvi.updateState { it.copy(loading = true) }
val auth = identityRepository.authToken.value
val refreshing = currentState.refreshing
val sort = keyStore[KeyStoreKeys.DefaultCommentSortType, 3].toSortType()
val commentList = postsRepository.getAll(
auth = auth,
communityId = community.id,
page = currentPage,
sort = sort,
)
currentPage++
val canFetchMore = commentList.size >= CommentRepository.DEFAULT_PAGE_SIZE
mvi.updateState {
val newItems = if (refreshing) {
commentList
} else {
it.posts + commentList
}
it.copy(
posts = newItems,
loading = false,
canFetchMore = canFetchMore,
refreshing = false,
)
}
}
}
private fun upVotePost(post: PostModel, value: Boolean) {
val newPost = postsRepository.asUpVoted(post, value)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
}
mvi.scope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.upVote(
auth = auth,
post = post,
voted = value,
)
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
post
} else {
p
}
},
)
}
}
}
}
private fun downVotePost(post: PostModel, value: Boolean) {
val newPost = postsRepository.asDownVoted(post, value)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
}
mvi.scope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.downVote(
auth = auth,
post = post,
downVoted = value,
)
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
post
} else {
p
}
},
)
}
}
}
}
private fun savePost(post: PostModel, value: Boolean) {
val newPost = postsRepository.asSaved(post, value)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
}
mvi.scope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.save(
auth = auth,
post = post,
saved = value,
)
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
post
} else {
p
}
},
)
}
}
}
}
}

View File

@ -1,16 +1,16 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.di
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreenMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreenViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreenMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreenViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailViewModel
import org.koin.dsl.module
val commonUiModule = module {
factory { params ->
PostDetailScreenViewModel(
mvi = DefaultMviModel(PostDetailScreenMviModel.UiState()),
PostDetailViewModel(
mvi = DefaultMviModel(PostDetailMviModel.UiState()),
post = params[0],
identityRepository = get(),
postsRepository = get(),
@ -20,8 +20,8 @@ val commonUiModule = module {
)
}
factory { params ->
CommunityDetailScreenViewModel(
mvi = DefaultMviModel(CommunityDetailScreenMviModel.UiState()),
CommunityDetailViewModel(
mvi = DefaultMviModel(CommunityDetailMviModel.UiState()),
community = params[0],
identityRepository = get(),
postsRepository = get(),

View File

@ -1,10 +1,10 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.di
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreenViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreenViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailViewModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
expect fun getPostDetailScreenViewModel(post: PostModel): PostDetailScreenViewModel
expect fun getPostDetailScreenViewModel(post: PostModel): PostDetailViewModel
expect fun getCommunityDetailScreenViewModel(community: CommunityModel): CommunityDetailScreenViewModel
expect fun getCommunityDetailScreenViewModel(community: CommunityModel): CommunityDetailViewModel

View File

@ -4,8 +4,8 @@ import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
interface PostDetailScreenMviModel :
MviModel<PostDetailScreenMviModel.Intent, PostDetailScreenMviModel.UiState, PostDetailScreenMviModel.Effect> {
interface PostDetailMviModel :
MviModel<PostDetailMviModel.Intent, PostDetailMviModel.UiState, PostDetailMviModel.Effect> {
sealed interface Intent {
object Refresh : Intent

View File

@ -75,7 +75,7 @@ class PostDetailScreen(
) { padding ->
val post = uiState.post
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
model.reduce(PostDetailScreenMviModel.Intent.Refresh)
model.reduce(PostDetailMviModel.Intent.Refresh)
})
Box(
modifier = Modifier.pullRefresh(pullRefreshState),
@ -111,13 +111,13 @@ class PostDetailScreen(
downVoted = post.myVote < 0,
saved = post.saved,
onUpVote = {
model.reduce(PostDetailScreenMviModel.Intent.UpVotePost(it, post))
model.reduce(PostDetailMviModel.Intent.UpVotePost(it, post))
},
onDownVote = {
model.reduce(PostDetailScreenMviModel.Intent.DownVotePost(it, post))
model.reduce(PostDetailMviModel.Intent.DownVotePost(it, post))
},
onSave = {
model.reduce(PostDetailScreenMviModel.Intent.SavePost(it, post))
model.reduce(PostDetailMviModel.Intent.SavePost(it, post))
},
)
}
@ -126,7 +126,7 @@ class PostDetailScreen(
comment = comment,
onUpVote = {
model.reduce(
PostDetailScreenMviModel.Intent.UpVoteComment(
PostDetailMviModel.Intent.UpVoteComment(
it,
comment,
),
@ -134,7 +134,7 @@ class PostDetailScreen(
},
onDownVote = {
model.reduce(
PostDetailScreenMviModel.Intent.DownVoteComment(
PostDetailMviModel.Intent.DownVoteComment(
it,
comment,
),
@ -142,7 +142,7 @@ class PostDetailScreen(
},
onSave = {
model.reduce(
PostDetailScreenMviModel.Intent.SaveComment(
PostDetailMviModel.Intent.SaveComment(
it,
comment,
),
@ -152,7 +152,7 @@ class PostDetailScreen(
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
model.reduce(PostDetailScreenMviModel.Intent.LoadNextPage)
model.reduce(PostDetailMviModel.Intent.LoadNextPage)
}
if (uiState.loading && !uiState.refreshing) {
Box(

View File

@ -1,223 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.KeyStoreKeys
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.TemporaryKeyStore
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toSortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostsRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
class PostDetailScreenViewModel(
private val mvi: DefaultMviModel<PostDetailScreenMviModel.Intent, PostDetailScreenMviModel.UiState, PostDetailScreenMviModel.Effect>,
private val post: PostModel,
private val identityRepository: IdentityRepository,
private val postsRepository: PostsRepository,
private val commentRepository: CommentRepository,
private val keyStore: TemporaryKeyStore,
private val notificationCenter: NotificationCenter,
) : MviModel<PostDetailScreenMviModel.Intent, PostDetailScreenMviModel.UiState, PostDetailScreenMviModel.Effect> by mvi,
ScreenModel {
private var currentPage: Int = 1
override fun onStarted() {
mvi.onStarted()
mvi.updateState { it.copy(post = post) }
if (mvi.uiState.value.comments.isEmpty()) {
refresh()
}
}
override fun reduce(intent: PostDetailScreenMviModel.Intent) {
when (intent) {
PostDetailScreenMviModel.Intent.LoadNextPage -> loadNextPage()
PostDetailScreenMviModel.Intent.Refresh -> refresh()
is PostDetailScreenMviModel.Intent.DownVoteComment -> downVoteComment(
intent.comment,
intent.value,
)
is PostDetailScreenMviModel.Intent.DownVotePost -> downVotePost(
intent.post,
intent.value,
)
is PostDetailScreenMviModel.Intent.SaveComment -> saveComment(
intent.comment,
intent.value,
)
is PostDetailScreenMviModel.Intent.SavePost -> savePost(
intent.post,
intent.value,
)
is PostDetailScreenMviModel.Intent.UpVoteComment -> upVoteComment(
intent.comment,
intent.value,
)
is PostDetailScreenMviModel.Intent.UpVotePost -> upVotePost(
intent.post,
intent.value,
)
}
}
private fun refresh() {
currentPage = 1
mvi.updateState { it.copy(canFetchMore = true, refreshing = true) }
loadNextPage()
}
private fun loadNextPage() {
val currentState = mvi.uiState.value
if (!currentState.canFetchMore || currentState.loading) {
return
}
mvi.scope.launch(Dispatchers.IO) {
mvi.updateState { it.copy(loading = true) }
val auth = identityRepository.authToken.value
val refreshing = currentState.refreshing
val sort = keyStore[KeyStoreKeys.DefaultCommentSortType, 3].toSortType()
val commentList = commentRepository.getAll(
auth = auth,
postId = post.id,
page = currentPage,
sort = sort,
)
currentPage++
val canFetchMore = commentList.size >= CommentRepository.DEFAULT_PAGE_SIZE
mvi.updateState {
val newcomments = if (refreshing) {
commentList
} else {
it.comments + commentList
}
it.copy(
comments = newcomments,
loading = false,
canFetchMore = canFetchMore,
refreshing = false,
)
}
}
}
private fun upVotePost(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newPost = postsRepository.upVote(
auth = auth,
post = post,
voted = value,
)
mvi.updateState { it.copy(post = newPost) }
notificationCenter.send(NotificationCenter.Event.PostUpdate(newPost))
}
}
private fun downVotePost(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newPost = postsRepository.downVote(
auth = auth,
post = post,
downVoted = value,
)
mvi.updateState { it.copy(post = newPost) }
notificationCenter.send(NotificationCenter.Event.PostUpdate(newPost))
}
}
private fun savePost(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newPost = postsRepository.save(
auth = auth,
post = post,
saved = value,
)
mvi.updateState { it.copy(post = newPost) }
notificationCenter.send(NotificationCenter.Event.PostUpdate(newPost))
}
}
private fun upVoteComment(comment: CommentModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newComment = commentRepository.upVote(
auth = auth,
comment = comment,
voted = value,
)
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
newComment
} else {
c
}
},
)
}
notificationCenter.send(NotificationCenter.Event.CommentUpdate(newComment))
}
}
private fun downVoteComment(comment: CommentModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newComment = commentRepository.downVote(
auth = auth,
comment = comment,
downVoted = value,
)
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
newComment
} else {
c
}
},
)
}
notificationCenter.send(NotificationCenter.Event.CommentUpdate(newComment))
}
}
private fun saveComment(comment: CommentModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newComment = commentRepository.save(
auth = auth,
comment = comment,
saved = value,
)
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
newComment
} else {
c
}
},
)
}
notificationCenter.send(NotificationCenter.Event.CommentUpdate(newComment))
}
}
}

View File

@ -0,0 +1,290 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.KeyStoreKeys
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.TemporaryKeyStore
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toSortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostsRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
class PostDetailViewModel(
private val mvi: DefaultMviModel<PostDetailMviModel.Intent, PostDetailMviModel.UiState, PostDetailMviModel.Effect>,
private val post: PostModel,
private val identityRepository: IdentityRepository,
private val postsRepository: PostsRepository,
private val commentRepository: CommentRepository,
private val keyStore: TemporaryKeyStore,
private val notificationCenter: NotificationCenter,
) : MviModel<PostDetailMviModel.Intent, PostDetailMviModel.UiState, PostDetailMviModel.Effect> by mvi,
ScreenModel {
private var currentPage: Int = 1
override fun onStarted() {
mvi.onStarted()
mvi.updateState { it.copy(post = post) }
if (mvi.uiState.value.comments.isEmpty()) {
refresh()
}
}
override fun reduce(intent: PostDetailMviModel.Intent) {
when (intent) {
PostDetailMviModel.Intent.LoadNextPage -> loadNextPage()
PostDetailMviModel.Intent.Refresh -> refresh()
is PostDetailMviModel.Intent.DownVoteComment -> downVoteComment(
intent.comment,
intent.value,
)
is PostDetailMviModel.Intent.DownVotePost -> downVotePost(
intent.post,
intent.value,
)
is PostDetailMviModel.Intent.SaveComment -> saveComment(
intent.comment,
intent.value,
)
is PostDetailMviModel.Intent.SavePost -> savePost(
intent.post,
intent.value,
)
is PostDetailMviModel.Intent.UpVoteComment -> upVoteComment(
intent.comment,
intent.value,
)
is PostDetailMviModel.Intent.UpVotePost -> upVotePost(
intent.post,
intent.value,
)
}
}
private fun refresh() {
currentPage = 1
mvi.updateState { it.copy(canFetchMore = true, refreshing = true) }
loadNextPage()
}
private fun loadNextPage() {
val currentState = mvi.uiState.value
if (!currentState.canFetchMore || currentState.loading) {
return
}
mvi.scope.launch(Dispatchers.IO) {
mvi.updateState { it.copy(loading = true) }
val auth = identityRepository.authToken.value
val refreshing = currentState.refreshing
val sort = keyStore[KeyStoreKeys.DefaultCommentSortType, 3].toSortType()
val commentList = commentRepository.getAll(
auth = auth,
postId = post.id,
page = currentPage,
sort = sort,
)
currentPage++
val canFetchMore = commentList.size >= CommentRepository.DEFAULT_PAGE_SIZE
mvi.updateState {
val newcomments = if (refreshing) {
commentList
} else {
it.comments + commentList
}
it.copy(
comments = newcomments,
loading = false,
canFetchMore = canFetchMore,
refreshing = false,
)
}
}
}
private fun upVotePost(post: PostModel, value: Boolean) {
val newPost = postsRepository.asUpVoted(post, value)
mvi.updateState { it.copy(post = newPost) }
mvi.scope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.upVote(
auth = auth,
post = post,
voted = value,
)
notificationCenter.send(NotificationCenter.Event.PostUpdate(newPost))
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState { it.copy(post = post) }
}
}
}
private fun downVotePost(post: PostModel, value: Boolean) {
val newPost = postsRepository.asDownVoted(post, value)
mvi.updateState { it.copy(post = newPost) }
mvi.scope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.downVote(
auth = auth,
post = post,
downVoted = value,
)
notificationCenter.send(NotificationCenter.Event.PostUpdate(newPost))
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState { it.copy(post = post) }
}
}
}
private fun savePost(post: PostModel, value: Boolean) {
val newPost = postsRepository.asSaved(post, value)
mvi.updateState { it.copy(post = newPost) }
mvi.scope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.save(
auth = auth,
post = post,
saved = value,
)
notificationCenter.send(NotificationCenter.Event.PostUpdate(newPost))
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState { it.copy(post = post) }
}
}
}
private fun upVoteComment(comment: CommentModel, value: Boolean) {
val newComment = commentRepository.asUpVoted(comment, value)
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
newComment
} else {
c
}
},
)
}
mvi.scope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
commentRepository.upVote(
auth = auth,
comment = comment,
voted = value,
)
notificationCenter.send(NotificationCenter.Event.CommentUpdate(newComment))
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
comment
} else {
c
}
},
)
}
}
}
}
private fun downVoteComment(comment: CommentModel, value: Boolean) {
val newComment = commentRepository.asDownVoted(comment, value)
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
newComment
} else {
c
}
},
)
}
mvi.scope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
commentRepository.downVote(
auth = auth,
comment = comment,
downVoted = value,
)
notificationCenter.send(NotificationCenter.Event.CommentUpdate(newComment))
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
comment
} else {
c
}
},
)
}
}
}
}
private fun saveComment(comment: CommentModel, value: Boolean) {
val newComment = commentRepository.asSaved(comment, value)
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
newComment
} else {
c
}
},
)
}
mvi.scope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
commentRepository.save(
auth = auth,
comment = comment,
saved = value,
)
notificationCenter.send(NotificationCenter.Event.CommentUpdate(newComment))
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
comment
} else {
c
}
},
)
}
}
}
}
}

View File

@ -1,30 +1,30 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.di
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreenViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreenViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailViewModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailViewModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.parameter.parametersOf
actual fun getPostDetailScreenViewModel(post: PostModel): PostDetailScreenViewModel =
actual fun getPostDetailScreenViewModel(post: PostModel): PostDetailViewModel =
PostDetailScreenViewModelHelper.getPostDetailModel(post)
actual fun getCommunityDetailScreenViewModel(community: CommunityModel): CommunityDetailScreenViewModel =
actual fun getCommunityDetailScreenViewModel(community: CommunityModel): CommunityDetailViewModel =
PostDetailScreenViewModelHelper.getCommunityDetailModel(community)
object PostDetailScreenViewModelHelper : KoinComponent {
fun getPostDetailModel(post: PostModel): PostDetailScreenViewModel {
val model: PostDetailScreenViewModel by inject(
fun getPostDetailModel(post: PostModel): PostDetailViewModel {
val model: PostDetailViewModel by inject(
parameters = { parametersOf(post) },
)
return model
}
fun getCommunityDetailModel(community: CommunityModel): CommunityDetailScreenViewModel {
val model: CommunityDetailScreenViewModel by inject(
fun getCommunityDetailModel(community: CommunityModel): CommunityDetailViewModel {
val model: CommunityDetailViewModel by inject(
parameters = { parametersOf(community) },
)
return model

View File

@ -36,49 +36,52 @@ class CommentRepository(
return dto.map { it.toModel() }
}
suspend fun upVote(comment: CommentModel, auth: String, voted: Boolean): CommentModel {
fun asUpVoted(comment: CommentModel, voted: Boolean) = comment.copy(
myVote = if (voted) 1 else 0,
score = when {
voted && comment.myVote < 0 -> comment.score + 2
voted -> comment.score + 1
!voted -> comment.score - 1
else -> comment.score
},
)
suspend fun upVote(comment: CommentModel, auth: String, voted: Boolean) {
val data = CreateCommentLikeForm(
commentId = comment.id,
score = if (voted) 1 else 0,
auth = auth,
)
services.comment.like(data)
return comment.copy(
myVote = if (voted) 1 else 0,
score = when {
voted && comment.myVote < 0 -> comment.score + 2
voted -> comment.score + 1
!voted -> comment.score - 1
else -> comment.score
},
)
}
suspend fun downVote(comment: CommentModel, auth: String, downVoted: Boolean): CommentModel {
fun asDownVoted(comment: CommentModel, downVoted: Boolean) = comment.copy(
myVote = if (downVoted) -1 else 0,
score = when {
downVoted && comment.myVote > 0 -> comment.score - 2
downVoted -> comment.score - 1
!downVoted -> comment.score + 1
else -> comment.score
},
)
suspend fun downVote(comment: CommentModel, auth: String, downVoted: Boolean) {
val data = CreateCommentLikeForm(
commentId = comment.id,
score = if (downVoted) -1 else 0,
auth = auth,
)
services.comment.like(data)
return comment.copy(
myVote = if (downVoted) -1 else 0,
score = when {
downVoted && comment.myVote > 0 -> comment.score - 2
downVoted -> comment.score - 1
!downVoted -> comment.score + 1
else -> comment.score
},
)
}
suspend fun save(comment: CommentModel, auth: String, saved: Boolean): CommentModel {
fun asSaved(comment: CommentModel, saved: Boolean) = comment.copy(saved = saved)
suspend fun save(comment: CommentModel, auth: String, saved: Boolean) {
val data = SaveCommentForm(
commentId = comment.id,
save = saved,
auth = auth,
)
services.comment.save(data)
return comment.copy(saved = saved)
}
}

View File

@ -37,49 +37,52 @@ class PostsRepository(
return dto.map { it.toModel() }
}
suspend fun upVote(post: PostModel, auth: String, voted: Boolean): PostModel {
fun asUpVoted(post: PostModel, voted: Boolean) = post.copy(
myVote = if (voted) 1 else 0,
score = when {
voted && post.myVote < 0 -> post.score + 2
voted -> post.score + 1
!voted -> post.score - 1
else -> post.score
},
)
suspend fun upVote(post: PostModel, auth: String, voted: Boolean) {
val data = CreatePostLikeForm(
postId = post.id,
score = if (voted) 1 else 0,
auth = auth,
)
services.post.like(data)
return post.copy(
myVote = if (voted) 1 else 0,
score = when {
voted && post.myVote < 0 -> post.score + 2
voted -> post.score + 1
!voted -> post.score - 1
else -> post.score
},
)
}
suspend fun downVote(post: PostModel, auth: String, downVoted: Boolean): PostModel {
fun asDownVoted(post: PostModel, downVoted: Boolean) = post.copy(
myVote = if (downVoted) -1 else 0,
score = when {
downVoted && post.myVote > 0 -> post.score - 2
downVoted -> post.score - 1
!downVoted -> post.score + 1
else -> post.score
},
)
suspend fun downVote(post: PostModel, auth: String, downVoted: Boolean) {
val data = CreatePostLikeForm(
postId = post.id,
score = if (downVoted) -1 else 0,
auth = auth,
)
services.post.like(data)
return post.copy(
myVote = if (downVoted) -1 else 0,
score = when {
downVoted && post.myVote > 0 -> post.score - 2
downVoted -> post.score - 1
!downVoted -> post.score + 1
else -> post.score
},
)
}
suspend fun save(post: PostModel, auth: String, saved: Boolean): PostModel {
fun asSaved(post: PostModel, saved: Boolean): PostModel = post.copy(saved = saved)
suspend fun save(post: PostModel, auth: String, saved: Boolean) {
val data = SavePostForm(
postId = post.id,
save = saved,
auth = auth,
)
services.post.save(data)
return post.copy(saved = saved)
}
}

View File

@ -1,9 +1,9 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.home.di
import com.github.diegoberaldin.raccoonforlemmy.feature.home.viewmodel.HomeScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.home.postlist.PostListViewModel
import org.koin.java.KoinJavaComponent.inject
actual fun getHomeScreenModel(): HomeScreenModel {
val res: HomeScreenModel by inject(HomeScreenModel::class.java)
actual fun getHomeScreenModel(): PostListViewModel {
val res: PostListViewModel by inject(PostListViewModel::class.java)
return res
}

View File

@ -3,8 +3,8 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.home.di
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.commonUiModule
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.di.postsRepositoryModule
import com.github.diegoberaldin.raccoonforlemmy.feature.home.viewmodel.HomeScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.home.viewmodel.HomeScreenMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.home.postlist.PostListViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.home.postlist.PostListMviModel
import org.koin.dsl.module
val homeTabModule = module {
@ -13,8 +13,8 @@ val homeTabModule = module {
commonUiModule,
)
factory {
HomeScreenModel(
mvi = DefaultMviModel(HomeScreenMviModel.UiState()),
PostListViewModel(
mvi = DefaultMviModel(PostListMviModel.UiState()),
postsRepository = get(),
apiConfigRepository = get(),
identityRepository = get(),

View File

@ -1,5 +1,5 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.home.di
import com.github.diegoberaldin.raccoonforlemmy.feature.home.viewmodel.HomeScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.home.postlist.PostListViewModel
expect fun getHomeScreenModel(): HomeScreenModel
expect fun getHomeScreenModel(): PostListViewModel

View File

@ -1,12 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.home.viewmodel
package com.github.diegoberaldin.raccoonforlemmy.feature.home.postlist
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
interface HomeScreenMviModel :
MviModel<HomeScreenMviModel.Intent, HomeScreenMviModel.UiState, HomeScreenMviModel.Effect> {
interface PostListMviModel :
MviModel<PostListMviModel.Intent, PostListMviModel.UiState, PostListMviModel.Effect> {
sealed interface Intent {
object Refresh : Intent

View File

@ -1,4 +1,4 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.home.ui
package com.github.diegoberaldin.raccoonforlemmy.feature.home.postlist
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -36,7 +36,6 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.ListingType
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SortBottomSheet
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.feature.home.di.getHomeScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.home.viewmodel.HomeScreenMviModel
class PostListScreen : Screen {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@ -60,7 +59,7 @@ class PostListScreen : Screen {
ListingTypeBottomSheet(
isLogged = uiState.isLogged,
onSelected = {
model.reduce(HomeScreenMviModel.Intent.ChangeListing(it))
model.reduce(PostListMviModel.Intent.ChangeListing(it))
},
onHide = {
bottomSheetNavigator.hide()
@ -72,7 +71,7 @@ class PostListScreen : Screen {
bottomSheetNavigator.show(
SortBottomSheet(
onSelected = {
model.reduce(HomeScreenMviModel.Intent.ChangeSort(it))
model.reduce(PostListMviModel.Intent.ChangeSort(it))
},
onHide = {
bottomSheetNavigator.hide()
@ -84,7 +83,7 @@ class PostListScreen : Screen {
},
) { padding ->
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
model.reduce(HomeScreenMviModel.Intent.Refresh)
model.reduce(PostListMviModel.Intent.Refresh)
})
Box(
modifier = Modifier.padding(padding).pullRefresh(pullRefreshState),
@ -117,19 +116,19 @@ class PostListScreen : Screen {
)
},
onUpVote = {
model.reduce(HomeScreenMviModel.Intent.UpVotePost(it, post))
model.reduce(PostListMviModel.Intent.UpVotePost(it, post))
},
onDownVote = {
model.reduce(HomeScreenMviModel.Intent.DownVotePost(it, post))
model.reduce(PostListMviModel.Intent.DownVotePost(it, post))
},
onSave = {
model.reduce(HomeScreenMviModel.Intent.SavePost(it, post))
model.reduce(PostListMviModel.Intent.SavePost(it, post))
},
)
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
model.reduce(HomeScreenMviModel.Intent.LoadNextPage)
model.reduce(PostListMviModel.Intent.LoadNextPage)
}
if (uiState.loading && !uiState.refreshing) {
Box(

View File

@ -1,4 +1,4 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.home.viewmodel
package com.github.diegoberaldin.raccoonforlemmy.feature.home.postlist
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
@ -22,27 +22,27 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class HomeScreenModel(
private val mvi: DefaultMviModel<HomeScreenMviModel.Intent, HomeScreenMviModel.UiState, HomeScreenMviModel.Effect>,
class PostListViewModel(
private val mvi: DefaultMviModel<PostListMviModel.Intent, PostListMviModel.UiState, PostListMviModel.Effect>,
private val postsRepository: PostsRepository,
private val apiConfigRepository: ApiConfigurationRepository,
private val identityRepository: IdentityRepository,
private val keyStore: TemporaryKeyStore,
private val notificationCenter: NotificationCenter,
) : ScreenModel,
MviModel<HomeScreenMviModel.Intent, HomeScreenMviModel.UiState, HomeScreenMviModel.Effect> by mvi {
MviModel<PostListMviModel.Intent, PostListMviModel.UiState, PostListMviModel.Effect> by mvi {
private var currentPage: Int = 1
override fun reduce(intent: HomeScreenMviModel.Intent) {
override fun reduce(intent: PostListMviModel.Intent) {
when (intent) {
HomeScreenMviModel.Intent.LoadNextPage -> loadNextPage()
HomeScreenMviModel.Intent.Refresh -> refresh()
is HomeScreenMviModel.Intent.ChangeSort -> applySortType(intent.value)
is HomeScreenMviModel.Intent.ChangeListing -> applyListingType(intent.value)
is HomeScreenMviModel.Intent.DownVotePost -> downVote(intent.post, intent.value)
is HomeScreenMviModel.Intent.SavePost -> save(intent.post, intent.value)
is HomeScreenMviModel.Intent.UpVotePost -> upVote(intent.post, intent.value)
PostListMviModel.Intent.LoadNextPage -> loadNextPage()
PostListMviModel.Intent.Refresh -> refresh()
is PostListMviModel.Intent.ChangeSort -> applySortType(intent.value)
is PostListMviModel.Intent.ChangeListing -> applyListingType(intent.value)
is PostListMviModel.Intent.DownVotePost -> downVote(intent.post, intent.value)
is PostListMviModel.Intent.SavePost -> save(intent.post, intent.value)
is PostListMviModel.Intent.UpVotePost -> upVote(intent.post, intent.value)
}
}
@ -143,67 +143,115 @@ class HomeScreenModel(
}
private fun upVote(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newPost = postsRepository.upVote(
post = post,
auth = auth,
voted = value,
val newPost = postsRepository.asUpVoted(post, value)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
}
mvi.scope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.upVote(
post = post,
auth = auth,
voted = value,
)
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
post
} else {
p
}
},
)
}
}
}
}
private fun downVote(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newPost = postsRepository.downVote(
post = post,
auth = auth,
downVoted = value,
val newPost = postsRepository.asDownVoted(post, value)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
}
mvi.scope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.downVote(
post = post,
auth = auth,
downVoted = value,
)
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
post
} else {
p
}
},
)
}
}
}
}
private fun save(post: PostModel, value: Boolean) {
mvi.scope.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
val newPost = postsRepository.save(
post = post,
auth = auth,
saved = value,
val newPost = postsRepository.asSaved(post, value)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
}
mvi.scope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.save(
post = post,
auth = auth,
saved = value,
)
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
post
} else {
p
}
},
)
}
}
}
}

View File

@ -1,4 +1,4 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.home.ui
package com.github.diegoberaldin.raccoonforlemmy.feature.home.postlist
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement

View File

@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.feature.home.postlist.PostListScreen
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.resources.di.getLanguageRepository
import com.github.diegoberaldin.raccoonforlemmy.resources.di.staticString

View File

@ -1,11 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.home.di
import com.github.diegoberaldin.raccoonforlemmy.feature.home.viewmodel.HomeScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.home.postlist.PostListViewModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
actual fun getHomeScreenModel() = HomeScreenModelHelper.model
object HomeScreenModelHelper : KoinComponent {
val model: HomeScreenModel by inject()
val model: PostListViewModel by inject()
}

View File

@ -5,12 +5,12 @@ import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.P
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments.ProfileCommentsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts.ProfilePostsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.viewmodel.ProfileScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.ProfileContentViewModel
import org.koin.core.parameter.parametersOf
import org.koin.java.KoinJavaComponent.inject
actual fun getProfileScreenModel(): ProfileScreenModel {
val res: ProfileScreenModel by inject(ProfileScreenModel::class.java)
actual fun getProfileScreenModel(): ProfileContentViewModel {
val res: ProfileContentViewModel by inject(ProfileContentViewModel::class.java)
return res
}

View File

@ -1,10 +1,10 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.viewmodel
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
interface ProfileScreenMviModel :
MviModel<ProfileScreenMviModel.Intent, ProfileScreenMviModel.UiState, ProfileScreenMviModel.Effect> {
interface ProfileContentMviModel :
MviModel<ProfileContentMviModel.Intent, ProfileContentMviModel.UiState, ProfileContentMviModel.Effect> {
sealed interface Intent {
object Logout : Intent

View File

@ -1,4 +1,4 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.ui
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
@ -28,7 +28,6 @@ import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.P
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.notlogged.ProfileNotLoggedContent
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.di.getProfileScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheet
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.viewmodel.ProfileScreenMviModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.resources.di.getLanguageRepository
import com.github.diegoberaldin.raccoonforlemmy.resources.di.staticString
@ -61,7 +60,7 @@ internal class ProfileContentScreen : Screen {
if (uiState.currentUser != null) {
Image(
modifier = Modifier.onClick {
model.reduce(ProfileScreenMviModel.Intent.Logout)
model.reduce(ProfileContentMviModel.Intent.Logout)
},
imageVector = Icons.Default.Logout,
contentDescription = null,

View File

@ -1,4 +1,4 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.viewmodel
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
@ -11,12 +11,12 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class ProfileScreenModel(
private val mvi: DefaultMviModel<ProfileScreenMviModel.Intent, ProfileScreenMviModel.UiState, ProfileScreenMviModel.Effect>,
class ProfileContentViewModel(
private val mvi: DefaultMviModel<ProfileContentMviModel.Intent, ProfileContentMviModel.UiState, ProfileContentMviModel.Effect>,
private val identityRepository: IdentityRepository,
private val siteRepository: SiteRepository,
) : ScreenModel,
MviModel<ProfileScreenMviModel.Intent, ProfileScreenMviModel.UiState, ProfileScreenMviModel.Effect> by mvi {
MviModel<ProfileContentMviModel.Intent, ProfileContentMviModel.UiState, ProfileContentMviModel.Effect> by mvi {
override fun onStarted() {
mvi.onStarted()
@ -35,9 +35,9 @@ class ProfileScreenModel(
}.launchIn(mvi.scope)
}
override fun reduce(intent: ProfileScreenMviModel.Intent) {
override fun reduce(intent: ProfileContentMviModel.Intent) {
when (intent) {
ProfileScreenMviModel.Intent.Logout -> identityRepository.clearToken()
ProfileContentMviModel.Intent.Logout -> identityRepository.clearToken()
}
}

View File

@ -9,14 +9,14 @@ import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.p
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts.ProfilePostsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.viewmodel.ProfileScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.viewmodel.ProfileScreenMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.ProfileContentViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.ProfileContentMviModel
import org.koin.dsl.module
val profileTabModule = module {
factory {
ProfileScreenModel(
mvi = DefaultMviModel(ProfileScreenMviModel.UiState()),
ProfileContentViewModel(
mvi = DefaultMviModel(ProfileContentMviModel.UiState()),
identityRepository = get(),
siteRepository = get(),
)

View File

@ -5,9 +5,9 @@ import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.P
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments.ProfileCommentsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts.ProfilePostsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.viewmodel.ProfileScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.ProfileContentViewModel
expect fun getProfileScreenModel(): ProfileScreenModel
expect fun getProfileScreenModel(): ProfileContentViewModel
expect fun getLoginBottomSheetViewModel(): LoginBottomSheetViewModel

View File

@ -11,6 +11,7 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.di.getApiConfigurationRepository
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.ProfileContentScreen
object ProfileTab : Tab {

View File

@ -5,7 +5,7 @@ import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.P
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments.ProfileCommentsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts.ProfilePostsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.viewmodel.ProfileScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.ProfileContentViewModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.parameter.parametersOf
@ -27,7 +27,7 @@ actual fun getProfileCommentsViewModel(user: UserModel): ProfileCommentsViewMode
ProfileScreenModelHelper.getCommentsModel(user)
object ProfileScreenModelHelper : KoinComponent {
val profileModel: ProfileScreenModel by inject()
val profileModel: ProfileContentViewModel by inject()
val loginModel: LoginBottomSheetViewModel by inject()
val loggedModel: ProfileLoggedViewModel by inject()

View File

@ -1,9 +1,9 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.di
import com.github.diegoberaldin.raccoonforlemmy.feature.search.viewmodel.SearchScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.communitylist.CommunityListViewModel
import org.koin.java.KoinJavaComponent.inject
actual fun getSearchScreenModel(): SearchScreenModel {
val res: SearchScreenModel by inject(SearchScreenModel::class.java)
actual fun getSearchScreenModel(): CommunityListViewModel {
val res: CommunityListViewModel by inject(CommunityListViewModel::class.java)
return res
}

View File

@ -1,10 +1,10 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.viewmodel
package com.github.diegoberaldin.raccoonforlemmy.feature.search.communitylist
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
interface SearchScreenMviModel :
MviModel<SearchScreenMviModel.Intent, SearchScreenMviModel.UiState, SearchScreenMviModel.Effect> {
interface CommunityListMviModel :
MviModel<CommunityListMviModel.Intent, CommunityListMviModel.UiState, CommunityListMviModel.Effect> {
sealed interface Intent {
object Refresh : Intent
object LoadNextPage : Intent

View File

@ -1,4 +1,4 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.ui
package com.github.diegoberaldin.raccoonforlemmy.feature.search.communitylist
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -40,7 +40,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.feature.search.di.getSearchScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.viewmodel.SearchScreenMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.ui.CommunityItem
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
@ -71,7 +71,7 @@ class CommunityListScreen : Screen {
keyboardType = KeyboardType.Text,
),
onValueChange = { value ->
model.reduce(SearchScreenMviModel.Intent.SetSearch(value))
model.reduce(CommunityListMviModel.Intent.SetSearch(value))
},
)
Row(
@ -82,7 +82,7 @@ class CommunityListScreen : Screen {
Checkbox(
checked = uiState.subscribedOnly,
onCheckedChange = {
model.reduce(SearchScreenMviModel.Intent.SetSubscribedOnly(it))
model.reduce(CommunityListMviModel.Intent.SetSubscribedOnly(it))
},
colors = CheckboxDefaults.colors(checkedColor = MaterialTheme.colorScheme.primary),
)
@ -93,7 +93,7 @@ class CommunityListScreen : Screen {
}
Spacer(modifier = Modifier.weight(1f))
Button(onClick = {
model.reduce(SearchScreenMviModel.Intent.SearchFired)
model.reduce(CommunityListMviModel.Intent.SearchFired)
}) {
Text(
text = stringResource(MR.strings.button_search),
@ -103,7 +103,7 @@ class CommunityListScreen : Screen {
}
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
model.reduce(SearchScreenMviModel.Intent.Refresh)
model.reduce(CommunityListMviModel.Intent.Refresh)
})
Box(
modifier = Modifier.padding(Spacing.xxs).pullRefresh(pullRefreshState),
@ -129,7 +129,7 @@ class CommunityListScreen : Screen {
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
model.reduce(SearchScreenMviModel.Intent.LoadNextPage)
model.reduce(CommunityListMviModel.Intent.LoadNextPage)
}
if (uiState.loading && !uiState.refreshing) {
Box(

View File

@ -1,4 +1,4 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.viewmodel
package com.github.diegoberaldin.raccoonforlemmy.feature.search.communitylist
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
@ -14,13 +14,13 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class SearchScreenModel(
private val mvi: DefaultMviModel<SearchScreenMviModel.Intent, SearchScreenMviModel.UiState, SearchScreenMviModel.Effect>,
class CommunityListViewModel(
private val mvi: DefaultMviModel<CommunityListMviModel.Intent, CommunityListMviModel.UiState, CommunityListMviModel.Effect>,
private val apiConfigRepository: ApiConfigurationRepository,
private val identityRepository: IdentityRepository,
private val communityRepository: CommunityRepository,
) : ScreenModel,
MviModel<SearchScreenMviModel.Intent, SearchScreenMviModel.UiState, SearchScreenMviModel.Effect> by mvi {
MviModel<CommunityListMviModel.Intent, CommunityListMviModel.UiState, CommunityListMviModel.Effect> by mvi {
private var currentPage: Int = 1
@ -44,13 +44,13 @@ class SearchScreenModel(
}
}
override fun reduce(intent: SearchScreenMviModel.Intent) {
override fun reduce(intent: CommunityListMviModel.Intent) {
when (intent) {
SearchScreenMviModel.Intent.LoadNextPage -> loadNextPage()
SearchScreenMviModel.Intent.Refresh -> refresh()
SearchScreenMviModel.Intent.SearchFired -> refresh()
is SearchScreenMviModel.Intent.SetSearch -> setSearch(intent.value)
is SearchScreenMviModel.Intent.SetSubscribedOnly -> applySubscribedOnly(intent.value)
CommunityListMviModel.Intent.LoadNextPage -> loadNextPage()
CommunityListMviModel.Intent.Refresh -> refresh()
CommunityListMviModel.Intent.SearchFired -> refresh()
is CommunityListMviModel.Intent.SetSearch -> setSearch(intent.value)
is CommunityListMviModel.Intent.SetSubscribedOnly -> applySubscribedOnly(intent.value)
}
}

View File

@ -1,14 +1,14 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.di
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.viewmodel.SearchScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.viewmodel.SearchScreenMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.communitylist.CommunityListViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.communitylist.CommunityListMviModel
import org.koin.dsl.module
val searchTabModule = module {
factory {
SearchScreenModel(
mvi = DefaultMviModel(SearchScreenMviModel.UiState()),
CommunityListViewModel(
mvi = DefaultMviModel(CommunityListMviModel.UiState()),
apiConfigRepository = get(),
identityRepository = get(),
communityRepository = get(),

View File

@ -1,5 +1,5 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.di
import com.github.diegoberaldin.raccoonforlemmy.feature.search.viewmodel.SearchScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.communitylist.CommunityListViewModel
expect fun getSearchScreenModel(): SearchScreenModel
expect fun getSearchScreenModel(): CommunityListViewModel

View File

@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.feature.search.communitylist.CommunityListScreen
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.resources.di.getLanguageRepository
import com.github.diegoberaldin.raccoonforlemmy.resources.di.staticString

View File

@ -1,11 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.di
import com.github.diegoberaldin.raccoonforlemmy.feature.search.viewmodel.SearchScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.communitylist.CommunityListViewModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
actual fun getSearchScreenModel() = SearchScreenModelHelper.model
object SearchScreenModelHelper : KoinComponent {
val model: SearchScreenModel by inject()
val model: CommunityListViewModel by inject()
}