WIP perfil

This commit is contained in:
Erik Cavazos 2023-08-06 22:11:28 -06:00
parent 0ab027b635
commit 4a8d05c87b
19 changed files with 351 additions and 284 deletions

View File

@ -24,7 +24,6 @@ import com.isolaatti.databinding.FragmentFeedBinding
import com.isolaatti.drafts.ui.DraftsActivity import com.isolaatti.drafts.ui.DraftsActivity
import com.isolaatti.home.presentation.FeedViewModel import com.isolaatti.home.presentation.FeedViewModel
import com.isolaatti.posting.PostViewerActivity import com.isolaatti.posting.PostViewerActivity
import com.isolaatti.posting.posts.presentation.PostsViewModel
import com.isolaatti.posting.comments.presentation.BottomSheetPostComments import com.isolaatti.posting.comments.presentation.BottomSheetPostComments
import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback
import com.isolaatti.posting.common.options_bottom_sheet.domain.OptionClicked import com.isolaatti.posting.common.options_bottom_sheet.domain.OptionClicked
@ -32,6 +31,7 @@ import com.isolaatti.posting.common.options_bottom_sheet.domain.Options
import com.isolaatti.posting.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel import com.isolaatti.posting.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel
import com.isolaatti.posting.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment import com.isolaatti.posting.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment
import com.isolaatti.posting.posts.presentation.PostsRecyclerViewAdapter import com.isolaatti.posting.posts.presentation.PostsRecyclerViewAdapter
import com.isolaatti.posting.posts.presentation.UpdateEvent
import com.isolaatti.posting.posts.ui.CreatePostActivity import com.isolaatti.posting.posts.ui.CreatePostActivity
import com.isolaatti.profile.ui.ProfileActivity import com.isolaatti.profile.ui.ProfileActivity
import com.isolaatti.settings.ui.SettingsActivity import com.isolaatti.settings.ui.SettingsActivity
@ -53,9 +53,8 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
const val CALLER_ID = 20 const val CALLER_ID = 20
} }
private val viewModel: PostsViewModel by activityViewModels()
private val errorViewModel: ErrorMessageViewModel by activityViewModels() private val errorViewModel: ErrorMessageViewModel by activityViewModels()
private val screenViewModel: FeedViewModel by viewModels() private val viewModel: FeedViewModel by activityViewModels()
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels() val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
private lateinit var viewBinding: FragmentFeedBinding private lateinit var viewBinding: FragmentFeedBinding
@ -158,7 +157,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
} }
} }
screenViewModel.userProfile.observe(viewLifecycleOwner) { viewModel.userProfile.observe(viewLifecycleOwner) {
val header = viewBinding.homeDrawer.getHeaderView(0) as? ConstraintLayout val header = viewBinding.homeDrawer.getHeaderView(0) as? ConstraintLayout
val image: ImageView? = header?.findViewById(R.id.profileImageView) val image: ImageView? = header?.findViewById(R.id.profileImageView)
@ -175,34 +174,23 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
viewModel.posts.observe(viewLifecycleOwner){ viewModel.posts.observe(viewLifecycleOwner){
if (it != null) { if (it.first != null) {
adapter.updateList(it,null) adapter.updateList(it.first!!, it.second)
} }
} }
// viewModel.loadingPosts.observe(viewLifecycleOwner) { viewModel.loadingPosts.observe(viewLifecycleOwner) {
// viewBinding.progressBarLoading.visibility = if(it) View.VISIBLE else View.GONE viewBinding.swipeToRefresh.isRefreshing = it
// viewBinding.loadMoreButton.visibility = if(it) View.GONE else View.VISIBLE }
// }
//
// viewModel.noMoreContent.observe(viewLifecycleOwner) { // viewModel.noMoreContent.observe(viewLifecycleOwner) {
// val visibility = if(it) View.VISIBLE else View.GONE //
// viewBinding.noMoreContentToShowTextView.visibility = visibility
// viewBinding.refreshButton.visibility = visibility
// viewBinding.loadMoreButton.visibility = if(it) View.GONE else View.VISIBLE
// } // }
viewModel.errorLoading.observe(viewLifecycleOwner) { viewModel.errorLoading.observe(viewLifecycleOwner) {
errorViewModel.error.postValue(it) errorViewModel.error.postValue(it)
} }
viewModel.postLiked.observe(viewLifecycleOwner) { viewModel.getProfile()
viewModel.posts.value?.let { feed ->
adapter.updateList(
feed, PostsRecyclerViewAdapter.UpdateEvent(
PostsRecyclerViewAdapter.UpdateEvent.UpdateType.POST_LIKED, it.postId))
}
}
screenViewModel.getProfile()
optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver) optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver)
} }
@ -210,8 +198,6 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
fun onNewMenuItemClicked(v: View) { fun onNewMenuItemClicked(v: View) {
} }
override fun onLiked(postId: Long) = viewModel.likePost(postId) override fun onLiked(postId: Long) = viewModel.likePost(postId)
override fun onUnLiked(postId: Long) = viewModel.unLikePost(postId) override fun onUnLiked(postId: Long) = viewModel.unLikePost(postId)
@ -234,5 +220,4 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
override fun onProfileClick(userId: Int) { override fun onProfileClick(userId: Int) {
ProfileActivity.startActivity(requireContext(), userId) ProfileActivity.startActivity(requireContext(), userId)
} }
} }

View File

@ -8,16 +8,13 @@ import androidx.lifecycle.Observer
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.common.IsolaattiBaseActivity import com.isolaatti.common.IsolaattiBaseActivity
import com.isolaatti.databinding.ActivityHomeBinding import com.isolaatti.databinding.ActivityHomeBinding
import com.isolaatti.posting.posts.presentation.PostsViewModel import com.isolaatti.home.presentation.FeedViewModel
import com.isolaatti.utils.Resource
import dagger.hilt.android.AndroidEntryPoint
class HomeActivity : IsolaattiBaseActivity() { class HomeActivity : IsolaattiBaseActivity() {
private lateinit var viewBinding: ActivityHomeBinding private lateinit var viewBinding: ActivityHomeBinding
private val postsViewModel: PostsViewModel by viewModels() private val feedViewModel: FeedViewModel by viewModels()
override fun onRetry() { override fun onRetry() {
} }
@ -32,7 +29,7 @@ class HomeActivity : IsolaattiBaseActivity() {
viewBinding.navigationRail?.setupWithNavController(navHostFragment.navController) viewBinding.navigationRail?.setupWithNavController(navHostFragment.navController)
if(savedInstanceState == null) { if(savedInstanceState == null) {
postsViewModel.getFeed(false) feedViewModel.getFeed(false)
} }
} }

View File

@ -5,6 +5,9 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.isolaatti.auth.domain.AuthRepository import com.isolaatti.auth.domain.AuthRepository
import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.posting.posts.presentation.PostListingViewModelBase
import com.isolaatti.posting.posts.presentation.UpdateEvent
import com.isolaatti.profile.data.remote.UserProfileDto import com.isolaatti.profile.data.remote.UserProfileDto
import com.isolaatti.profile.domain.use_case.GetProfile import com.isolaatti.profile.domain.use_case.GetProfile
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
@ -17,7 +20,36 @@ import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class FeedViewModel @Inject constructor(private val getProfileUseCase: GetProfile, private val authRepository: AuthRepository) : ViewModel() { class FeedViewModel @Inject constructor(
private val getProfileUseCase: GetProfile,
private val authRepository: AuthRepository,
private val postsRepository: PostsRepository
) : PostListingViewModelBase() {
override fun getFeed(refresh: Boolean) {
viewModelScope.launch {
if (refresh) {
posts.value = null
}
postsRepository.getFeed(getLastId()).onEach { feedDtoResource ->
when (feedDtoResource) {
is Resource.Success -> {
loadingPosts.postValue(false)
posts.postValue(Pair(posts.value?.first?.concatFeed(feedDtoResource.data) ?: feedDtoResource.data, UpdateEvent(UpdateEvent.UpdateType.PAGE_ADDED, null)))
noMoreContent.postValue(feedDtoResource.data?.moreContent == false)
}
is Resource.Loading -> {
loadingPosts.postValue(true)
}
is Resource.Error -> {
errorLoading.postValue(feedDtoResource.errorType)
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
// User profile // User profile
private val _userProfile: MutableLiveData<UserProfileDto> = MutableLiveData() private val _userProfile: MutableLiveData<UserProfileDto> = MutableLiveData()

View File

@ -10,14 +10,8 @@ import com.isolaatti.posting.posts.data.remote.PostApi
import com.isolaatti.posting.posts.domain.PostsRepository import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import retrofit2.await
import java.io.IOException
import java.lang.RuntimeException
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, private val postApi: PostApi) : PostsRepository { class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, private val postApi: PostApi) : PostsRepository {
override fun getFeed(lastId: Long): Flow<Resource<FeedDto>> = flow { override fun getFeed(lastId: Long): Flow<Resource<FeedDto>> = flow {
@ -39,7 +33,7 @@ class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, pr
} }
} }
override fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto): Flow<Resource<FeedDto>> = flow { override fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto?): Flow<Resource<FeedDto>> = flow {
emit(Resource.Loading()) emit(Resource.Loading())
try { try {
val gson = Gson() val gson = Gson()

View File

@ -11,7 +11,7 @@ interface PostsRepository {
fun getFeed(lastId: Long): Flow<Resource<FeedDto>> fun getFeed(lastId: Long): Flow<Resource<FeedDto>>
fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto): Flow<Resource<FeedDto>> fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto?): Flow<Resource<FeedDto>>
fun makePost(createPostDto: CreatePostDto): Flow<Resource<FeedDto.PostDto>> fun makePost(createPostDto: CreatePostDto): Flow<Resource<FeedDto.PostDto>>
fun editPost(editPostDto: EditPostDto): Flow<Resource<FeedDto.PostDto>> fun editPost(editPostDto: EditPostDto): Flow<Resource<FeedDto.PostDto>>

View File

@ -0,0 +1,22 @@
package com.isolaatti.posting.posts.presentation
import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback
abstract class PostListingRecyclerViewAdapterWiring(private val postsViewModelBase: PostListingViewModelBase) : OnUserInteractedWithPostCallback {
override fun onLiked(postId: Long) {
postsViewModelBase.likePost(postId)
}
override fun onUnLiked(postId: Long) {
postsViewModelBase.unLikePost(postId)
}
abstract override fun onComment(postId: Long)
abstract override fun onOpenPost(postId: Long)
abstract override fun onOptions(postId: Long)
abstract override fun onProfileClick(userId: Int)
}

View File

@ -0,0 +1,72 @@
package com.isolaatti.posting.posts.presentation
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isolaatti.posting.likes.data.remote.LikeDto
import com.isolaatti.posting.likes.domain.repository.LikesRepository
import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.profile.domain.use_case.GetProfilePosts
import com.isolaatti.utils.Resource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
abstract class PostListingViewModelBase : ViewModel() {
@Inject
lateinit var likesRepository: LikesRepository
@Inject
lateinit var getProfilePosts: GetProfilePosts
val posts: MutableLiveData<Pair<FeedDto?, UpdateEvent>> = MutableLiveData()
val loadingPosts = MutableLiveData(false)
val noMoreContent = MutableLiveData(false)
val errorLoading: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData()
fun getLastId(): Long = try { posts.value?.first?.data?.last()?.post?.id ?: 0 } catch (e: NoSuchElementException) { 0 }
abstract fun getFeed(refresh: Boolean)
fun likePost(postId: Long) {
viewModelScope.launch {
likesRepository.likePost(postId).onEach {likeDto ->
val likedPost = posts.value?.first?.data?.find { post -> post.post.id == likeDto.postId }
val index = posts.value?.first?.data?.indexOf(likedPost)
if(index != null){
val temp = posts.value?.first
Log.d("***", temp.toString())
temp?.data?.set(index, likedPost!!.apply {
liked = true
numberOfLikes = likeDto.likesCount
})
}
posts.postValue(posts.value?.copy(second = UpdateEvent(UpdateEvent.UpdateType.POST_LIKED, likedPost?.post?.id)))
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
fun unLikePost(postId: Long) {
viewModelScope.launch {
likesRepository.unLikePost(postId).onEach {likeDto ->
val likedPost = posts.value?.first?.data?.find { post -> post.post.id == likeDto.postId }
val index = posts.value?.first?.data?.indexOf(likedPost)
if(index != null){
posts.value?.first?.data?.set(index, likedPost!!.apply {
liked = false
numberOfLikes = likeDto.likesCount
})
}
posts.postValue(posts.value?.copy(second = UpdateEvent(UpdateEvent.UpdateType.POST_LIKED, likedPost?.post?.id)))
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
}

View File

@ -114,15 +114,6 @@ class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callba
data class LikeCountUpdatePayload(val likeCount: Int) data class LikeCountUpdatePayload(val likeCount: Int)
data class CommentsCountUpdatePayload(val commentsCount: Int) data class CommentsCountUpdatePayload(val commentsCount: Int)
data class UpdateEvent(val updateType: UpdateType, val affectedId: Long) {
enum class UpdateType {
POST_LIKED,
POST_COMMENTED,
POST_REMOVED,
POST_ADDED
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.post_layout, parent, false) val view = LayoutInflater.from(parent.context).inflate(R.layout.post_layout, parent, false)
@ -143,24 +134,34 @@ class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callba
notifyDataSetChanged() notifyDataSetChanged()
return return
} }
val postUpdated = feedDto?.data?.find { p -> p.post.id == updateEvent.affectedId } ?: return val postUpdated = feedDto?.data?.find { p -> p.post.id == updateEvent.affectedId }
val position = feedDto?.data?.indexOf(postUpdated) ?: return val position = feedDto?.data?.indexOf(postUpdated)
feedDto = updatedFeed feedDto = updatedFeed
when(updateEvent.updateType) { when(updateEvent.updateType) {
UpdateEvent.UpdateType.POST_LIKED -> { UpdateEvent.UpdateType.POST_LIKED -> {
if(postUpdated != null && position != null)
notifyItemChanged(position, LikeCountUpdatePayload(postUpdated.numberOfLikes)) notifyItemChanged(position, LikeCountUpdatePayload(postUpdated.numberOfLikes))
} }
UpdateEvent.UpdateType.POST_COMMENTED -> { UpdateEvent.UpdateType.POST_COMMENTED -> {
if(postUpdated != null && position != null)
notifyItemChanged(position, CommentsCountUpdatePayload(postUpdated.numberOfComments)) notifyItemChanged(position, CommentsCountUpdatePayload(postUpdated.numberOfComments))
} }
UpdateEvent.UpdateType.POST_REMOVED -> { UpdateEvent.UpdateType.POST_REMOVED -> {
if(postUpdated != null && position != null)
notifyItemRemoved(position) notifyItemRemoved(position)
} }
UpdateEvent.UpdateType.POST_ADDED -> { UpdateEvent.UpdateType.POST_ADDED -> {
notifyItemInserted(0) notifyItemInserted(0)
} }
UpdateEvent.UpdateType.PAGE_ADDED -> {
notifyItemInserted(itemCount - 1)
}
UpdateEvent.UpdateType.REFRESH -> {
notifyDataSetChanged()
}
} }
} }

View File

@ -1,105 +0,0 @@
package com.isolaatti.posting.posts.presentation
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isolaatti.posting.comments.data.remote.FeedCommentsDto
import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.likes.data.remote.LikeDto
import com.isolaatti.posting.likes.domain.repository.LikesRepository
import com.isolaatti.posting.posts.data.repository.PostsRepositoryImpl
import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.utils.Resource
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class PostsViewModel @Inject constructor(private val postsRepository: PostsRepository, private val likesRepository: LikesRepository) : ViewModel() {
private val _posts: MutableLiveData<FeedDto?> = MutableLiveData()
val posts: LiveData<FeedDto?> get() = _posts
private val _loadingPosts = MutableLiveData(false)
val loadingPosts: LiveData<Boolean> get() = _loadingPosts
private val _noMoreContent = MutableLiveData(false)
val noMoreContent: LiveData<Boolean> get() = _noMoreContent
private val _errorLoading: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData()
val errorLoading: LiveData<Resource.Error.ErrorType?> get() = _errorLoading
private val _comments: MutableLiveData<FeedCommentsDto> = MutableLiveData()
val comments: LiveData<FeedCommentsDto> get() = _comments
private fun getLastId(): Long = try {_posts.value?.data?.last()?.post?.id ?: 0} catch (e: NoSuchElementException) { 0 }
private val _postLiked: MutableLiveData<LikeDto> = MutableLiveData()
val postLiked: LiveData<LikeDto> get() = _postLiked
fun getFeed(refresh: Boolean) {
viewModelScope.launch {
if(refresh) {
_posts.value = null
}
postsRepository.getFeed(getLastId()).onEach {
when(it) {
is Resource.Success -> {
_loadingPosts.postValue(false)
_posts.postValue(posts.value?.concatFeed(it.data) ?: it.data)
_noMoreContent.postValue(it.data?.moreContent == false)
}
is Resource.Loading -> {
_loadingPosts.postValue(true)
}
is Resource.Error -> {
_errorLoading.postValue(it.errorType)
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
fun likePost(postId: Long) {
viewModelScope.launch {
likesRepository.likePost(postId).onEach {likeDto ->
val likedPost = _posts.value?.data?.find { post -> post.post.id == likeDto.postId }
val index = _posts.value?.data?.indexOf(likedPost)
Log.d("***", index.toString())
if(index != null){
val temp = _posts.value
Log.d("***", temp.toString())
temp?.data?.set(index, likedPost!!.apply {
liked = true
numberOfLikes = likeDto.likesCount
})
}
_postLiked.postValue(likeDto)
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
fun unLikePost(postId: Long) {
viewModelScope.launch {
likesRepository.unLikePost(postId).onEach {likeDto ->
val likedPost = _posts.value?.data?.find { post -> post.post.id == likeDto.postId }
val index = _posts.value?.data?.indexOf(likedPost)
if(index != null){
_posts.value?.data?.set(index, likedPost!!.apply {
liked = false
numberOfLikes = likeDto.likesCount
})
}
_postLiked.postValue(likeDto)
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
}

View File

@ -0,0 +1,12 @@
package com.isolaatti.posting.posts.presentation
data class UpdateEvent(val updateType: UpdateType, val affectedId: Long?) {
enum class UpdateType {
POST_LIKED,
POST_COMMENTED,
POST_REMOVED,
POST_ADDED,
PAGE_ADDED,
REFRESH
}
}

View File

@ -8,6 +8,6 @@ import kotlinx.coroutines.flow.Flow
import javax.inject.Inject import javax.inject.Inject
class GetProfilePosts @Inject constructor(private val postsRepository: PostsRepository) { class GetProfilePosts @Inject constructor(private val postsRepository: PostsRepository) {
operator fun invoke(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto): Flow<Resource<FeedDto>> = operator fun invoke(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto?): Flow<Resource<FeedDto>> =
postsRepository.getProfilePosts(userId, lastId, olderFirst, filter) postsRepository.getProfilePosts(userId, lastId, olderFirst, filter)
} }

View File

@ -6,6 +6,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.isolaatti.posting.posts.data.remote.FeedDto import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.data.remote.FeedFilterDto import com.isolaatti.posting.posts.data.remote.FeedFilterDto
import com.isolaatti.posting.posts.presentation.PostListingViewModelBase
import com.isolaatti.posting.posts.presentation.UpdateEvent
import com.isolaatti.profile.data.remote.UserProfileDto import com.isolaatti.profile.data.remote.UserProfileDto
import com.isolaatti.profile.domain.ProfileRepository import com.isolaatti.profile.domain.ProfileRepository
import com.isolaatti.profile.domain.use_case.GetProfile import com.isolaatti.profile.domain.use_case.GetProfile
@ -24,14 +26,13 @@ import java.time.Month
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ProfileViewModel @Inject constructor(private val getProfileUseCase: GetProfile, private val getProfilePosts: GetProfilePosts) : ViewModel() { class ProfileViewModel @Inject constructor(private val getProfileUseCase: GetProfile, private val getProfilePostsUseCase: GetProfilePosts) : PostListingViewModelBase() {
private val _profile = MutableLiveData<UserProfileDto>() private val _profile = MutableLiveData<UserProfileDto>()
val profile: LiveData<UserProfileDto> get() = _profile val profile: LiveData<UserProfileDto> get() = _profile
private val _posts = MutableLiveData<FeedDto>() var profileId: Int = 0
val posts: LiveData<FeedDto> get() = _posts
fun getProfile(profileId: Int) { fun getProfile() {
viewModelScope.launch { viewModelScope.launch {
getProfileUseCase(profileId).onEach { getProfileUseCase(profileId).onEach {
if(it is Resource.Success) { if(it is Resource.Success) {
@ -41,24 +42,23 @@ class ProfileViewModel @Inject constructor(private val getProfileUseCase: GetPro
} }
} }
fun getPosts(profileId: Int, refresh: Boolean) { override fun getFeed(refresh: Boolean) {
viewModelScope.launch { viewModelScope.launch {
getProfilePosts( getProfilePostsUseCase(profileId, getLastId(), false, null).onEach { feedDtoResource ->
profileId, when (feedDtoResource) {
-1, is Resource.Success -> {
false, loadingPosts.postValue(false)
FeedFilterDto( posts.postValue(Pair(posts.value?.first?.concatFeed(feedDtoResource.data) ?: feedDtoResource.data, UpdateEvent(UpdateEvent.UpdateType.PAGE_ADDED, null)))
"both", noMoreContent.postValue(feedDtoResource.data?.moreContent == false)
"both", }
FeedFilterDto.DataRange(
false, is Resource.Loading -> {
LocalDate.of(2020, 1, 1), loadingPosts.postValue(true)
LocalDate.now() }
)
) is Resource.Error -> {
).onEach { errorLoading.postValue(feedDtoResource.errorType)
if(it is Resource.Success) { }
_posts.postValue(it.data!!)
} }
}.flowOn(Dispatchers.IO).launchIn(this) }.flowOn(Dispatchers.IO).launchIn(this)
} }

View File

@ -1,29 +1,36 @@
package com.isolaatti.profile.ui package com.isolaatti.profile.ui
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.appbar.AppBarLayout
import com.isolaatti.BuildConfig import com.isolaatti.BuildConfig
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.databinding.FragmentDiscussionsBinding import com.isolaatti.databinding.FragmentDiscussionsBinding
import com.isolaatti.home.FeedFragment
import com.isolaatti.posting.PostViewerActivity
import com.isolaatti.posting.comments.presentation.BottomSheetPostComments
import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback
import com.isolaatti.posting.common.options_bottom_sheet.domain.Options
import com.isolaatti.posting.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel
import com.isolaatti.posting.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment
import com.isolaatti.posting.posts.data.remote.FeedDto import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.presentation.PostListingRecyclerViewAdapterWiring
import com.isolaatti.posting.posts.presentation.PostsRecyclerViewAdapter import com.isolaatti.posting.posts.presentation.PostsRecyclerViewAdapter
import com.isolaatti.posting.posts.presentation.UpdateEvent
import com.isolaatti.profile.data.remote.UserProfileDto import com.isolaatti.profile.data.remote.UserProfileDto
import com.isolaatti.profile.presentation.ProfileViewModel import com.isolaatti.profile.presentation.ProfileViewModel
import com.isolaatti.utils.PicassoImagesPluginDef import com.isolaatti.utils.PicassoImagesPluginDef
import com.isolaatti.utils.UrlGen import com.isolaatti.utils.UrlGen
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration import io.noties.markwon.MarkwonConfiguration
@ -31,14 +38,19 @@ import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAb
import io.noties.markwon.linkify.LinkifyPlugin import io.noties.markwon.linkify.LinkifyPlugin
@AndroidEntryPoint @AndroidEntryPoint
class DiscussionsFragment : Fragment(), OnUserInteractedWithPostCallback { class DiscussionsFragment : Fragment() {
lateinit var viewBinding: FragmentDiscussionsBinding lateinit var viewBinding: FragmentDiscussionsBinding
private val viewModel: ProfileViewModel by viewModels() private val viewModel: ProfileViewModel by viewModels()
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
private var userId: Int? = null private var userId: Int? = null
lateinit var postsAdapter: PostsRecyclerViewAdapter
// collapsing bar
private var title = "" private var title = ""
lateinit var feedAdapter: PostsRecyclerViewAdapter private var scrollRange = -1
private var isShow = false
private val profileObserver = Observer<UserProfileDto> { profile -> private val profileObserver = Observer<UserProfileDto> { profile ->
Picasso.get() Picasso.get()
@ -50,30 +62,18 @@ class DiscussionsFragment : Fragment(), OnUserInteractedWithPostCallback {
viewBinding.textViewDescription.text = profile.descriptionText viewBinding.textViewDescription.text = profile.descriptionText
} }
private val postsObserver: Observer<FeedDto> = Observer { private val postsObserver: Observer<Pair<FeedDto?, UpdateEvent>> = Observer {
feedAdapter.updateList(it, null) if(it.first != null) {
postsAdapter.updateList(it.first!!, it.second)
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userId = (requireActivity()).intent.extras?.getInt(ProfileActivity.EXTRA_USER_ID)
} }
override fun onCreateView( private lateinit var postListingRecyclerViewAdapterWiring: PostListingRecyclerViewAdapterWiring
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewBinding = FragmentDiscussionsBinding.inflate(inflater)
return viewBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { private fun setupCollapsingBar() {
super.onViewCreated(view, savedInstanceState)
var scrollRange = -1
var isShow = false
viewBinding.topAppBarLayout.addOnOffsetChangedListener { appBarLayout, verticalOffset -> viewBinding.topAppBarLayout.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
if (scrollRange == -1) scrollRange = appBarLayout.totalScrollRange if (scrollRange == -1) scrollRange = appBarLayout.totalScrollRange
if (scrollRange + verticalOffset == 0) { if (scrollRange + verticalOffset == 0) {
@ -83,38 +83,16 @@ class DiscussionsFragment : Fragment(), OnUserInteractedWithPostCallback {
viewBinding.collapsingToolbarLayout.title = " " viewBinding.collapsingToolbarLayout.title = " "
} }
} }
}
private fun bind() {
viewBinding.topAppBar.setNavigationOnClickListener { viewBinding.topAppBar.setNavigationOnClickListener {
findNavController().popBackStack() findNavController().popBackStack()
} }
viewModel.profile.observe(viewLifecycleOwner, profileObserver)
viewModel.posts.observe(viewLifecycleOwner, postsObserver)
userId?.let {
viewModel.getProfile(it)
viewModel.getPosts(it, true)
}
val markwon = Markwon.builder(requireContext())
.usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder
.imageDestinationProcessor(
ImageDestinationProcessorRelativeToAbsolute
.create(BuildConfig.backend))
}
})
.usePlugin(PicassoImagesPluginDef.picassoImagePlugin)
.usePlugin(LinkifyPlugin.create())
.build()
feedAdapter = PostsRecyclerViewAdapter(markwon, this, null)
viewBinding.feedRecyclerView.adapter = feedAdapter
viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
viewBinding.bottomAppBar.setOnMenuItemClickListener { viewBinding.bottomAppBar.setOnMenuItemClickListener {
when(it.itemId) { when(it.itemId) {
R.id.audios_menu_item -> { R.id.audios_menu_item -> {
@ -129,29 +107,88 @@ class DiscussionsFragment : Fragment(), OnUserInteractedWithPostCallback {
} }
} }
viewBinding.feedRecyclerView.adapter = postsAdapter
viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
} }
override fun onLiked(postId: Long) { private fun setObservers() {
TODO("Not yet implemented") viewModel.profile.observe(viewLifecycleOwner, profileObserver)
viewModel.posts.observe(viewLifecycleOwner, postsObserver)
} }
override fun onUnLiked(postId: Long) { private fun getData() {
TODO("Not yet implemented")
userId?.let { profileId ->
viewModel.profileId = profileId
viewModel.getProfile()
viewModel.getFeed(true)
}
} }
private fun setupPostsAdapter() {
val markwon = Markwon.builder(requireContext())
.usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder
.imageDestinationProcessor(
ImageDestinationProcessorRelativeToAbsolute
.create(BuildConfig.backend))
}
})
.usePlugin(PicassoImagesPluginDef.picassoImagePlugin)
.usePlugin(LinkifyPlugin.create())
.build()
postsAdapter = PostsRecyclerViewAdapter(markwon,postListingRecyclerViewAdapterWiring, null )
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userId = (requireActivity()).intent.extras?.getInt(ProfileActivity.EXTRA_USER_ID)
}
override fun onAttach(context: Context) {
super.onAttach(context)
postListingRecyclerViewAdapterWiring = object: PostListingRecyclerViewAdapterWiring(viewModel) {
override fun onComment(postId: Long) { override fun onComment(postId: Long) {
TODO("Not yet implemented") val modalBottomSheet = BottomSheetPostComments.getInstance(postId)
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostComments.TAG)
} }
override fun onOpenPost(postId: Long) { override fun onOpenPost(postId: Long) {
TODO("Not yet implemented") PostViewerActivity.startActivity(requireContext(), postId)
} }
override fun onOptions(postId: Long) { override fun onOptions(postId: Long) {
TODO("Not yet implemented") optionsViewModel.setOptions(Options.postOptions, FeedFragment.CALLER_ID)
val modalBottomSheet = BottomSheetPostOptionsFragment()
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG)
} }
override fun onProfileClick(userId: Int) { override fun onProfileClick(userId: Int) {
TODO("Not yet implemented") //ProfileActivity.startActivity(requireContext(), userId)
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewBinding = FragmentDiscussionsBinding.inflate(inflater)
return viewBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupCollapsingBar()
setupPostsAdapter()
bind()
setObservers()
getData()
} }
} }

View File

@ -10,6 +10,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContentProviderCompat.requireContext import androidx.core.content.ContentProviderCompat.requireContext
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.navigation.findNavController import androidx.navigation.findNavController
@ -45,7 +46,6 @@ class ProfileActivity : FragmentActivity() {
lateinit var viewBinding: ActivityProfileBinding lateinit var viewBinding: ActivityProfileBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewBinding = ActivityProfileBinding.inflate(layoutInflater) viewBinding = ActivityProfileBinding.inflate(layoutInflater)

View File

@ -1,8 +1,24 @@
package com.isolaatti.profile.ui package com.isolaatti.profile.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.isolaatti.databinding.FragmentUserlinkBinding
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class UserLinkFragment : Fragment() { class UserLinkFragment : Fragment() {
lateinit var binding: FragmentUserlinkBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentUserlinkBinding.inflate(inflater)
return binding.root
}
} }

View File

@ -1,17 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent"
xmlns:tools="http://schemas.android.com/tools" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/topAppBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/audios" app:title="@string/audios"
app:layout_constraintBottom_toBottomOf="parent" app:navigationIcon="@drawable/baseline_arrow_back_24"
app:layout_constraintEnd_toEndOf="parent" app:navigationIconTint="@color/on_surface"
app:layout_constraintStart_toStartOf="parent" app:titleCentered="true"/>
app:layout_constraintTop_toTopOf="parent" /> </com.google.android.material.appbar.AppBarLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -44,9 +44,10 @@
android:id="@+id/feed_recycler_view" android:id="@+id/feed_recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView <com.google.android.material.navigation.NavigationView

View File

@ -1,22 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent"
xmlns:tools="http://schemas.android.com/tools" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
<ScrollView android:id="@+id/topAppBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content"
app:title="@string/images"
<TextView app:navigationIcon="@drawable/baseline_arrow_back_24"
android:id="@+id/textView3" app:navigationIconTint="@color/on_surface"
android:layout_width="wrap_content" app:titleCentered="true"/>
android:layout_height="2000dp" </com.google.android.material.appbar.AppBarLayout>
android:text="@string/images" </androidx.coordinatorlayout.widget.CoordinatorLayout>
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ScrollView>
</FrameLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>